Skip to content
Draft

V3 #34

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ build
composer.lock
docs
vendor
coverage
.phpunit.result.cache
.phpunit.cache
12 changes: 12 additions & 0 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
build:
nodes:
analysis:
project_setup:
override: true
tests:
override: [php-scrutinizer-run]

filter:
excluded_paths: [tests/*]

Expand All @@ -17,3 +25,7 @@ checks:
fix_identation_4spaces: true
fix_doc_comments: true

tools:
external_code_coverage:
timeout: 600
runs: 1
2 changes: 1 addition & 1 deletion .styleci.yml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
preset: psr2
preset: psr12
20 changes: 13 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
language: php

php:
- 7.3
- 7.4
- 8.0

cache:
directories:
- $HOME/.composer/cache

env:
matrix:
- COMPOSER_FLAGS="--prefer-lowest"
- COMPOSER_FLAGS=""
- XDEBUG_MODE=coverage

before_script:
- travis_retry composer self-update
- travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source
- travis_retry composer update --no-interaction --prefer-dist

script:
- vendor/bin/phpcs --standard=psr2 src/
- vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover

after_script:
- php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover
- |
if [[ "$TRAVIS_PHP_VERSION" != '8.0' ]]; then
wget https://scrutinizer-ci.com/ocular.phar
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
fi
76 changes: 15 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

[K-mean](http://en.wikipedia.org/wiki/K-means_clustering) clustering algorithm implementation in PHP.

Please also see the [FAQ](#faq)

## Installation

You can install the package via composer:
Expand All @@ -22,8 +20,7 @@ composer require bdelespierre/php-kmeans
```PHP
require "vendor/autoload.php";

// prepare 50 points of 2D space to be clustered
$points = [
$data = [
[80,55],[86,59],[19,85],[41,47],[57,58],
[76,22],[94,60],[13,93],[90,48],[52,54],
[62,46],[88,44],[85,24],[63,14],[51,40],
Expand All @@ -37,30 +34,35 @@ $points = [
];

// create a 2-dimentions space
$space = new KMeans\Space(2);
$space = new Kmeans\Space(2);

// prepare the points
$points = new Kmeans\PointCollection($space);

// add points to space
foreach ($points as $i => $coordinates) {
$space->addPoint($coordinates);
foreach ($data as $coordinates) {
$points->attach(new Kmeans\Point($space, $coordinates));
}

// prepare the algorithm
$algorithm = new Kmeans\Algorithm(new Kmeans\RandomInitialization());

// cluster these 50 points in 3 clusters
$clusters = $space->solve(3);
$clusters = $algorithm->clusterize($points, 3);

// display the cluster centers and attached points
foreach ($clusters as $num => $cluster) {
$coordinates = $cluster->getCoordinates();
$coordinates = $cluster->getCentroid()->getCoordinates();
printf(
"Cluster %s [%d,%d]: %d points\n",
"Cluster #%s [%d,%d] has %d points\n",
$num,
$coordinates[0],
$coordinates[1],
count($cluster)
count($cluster->getPoints())
);
}
```

**Note:** the example is given with points of a 2D space but it will work with any dimention >1.
**Note:** the example is given with points of a 2D space but it will work with any dimention greater than or equal to 1.

### Testing

Expand Down Expand Up @@ -89,51 +91,3 @@ If you discover any security related issues, please email benjamin.delespierre@g
## License

Lesser General Public License (LGPL). Please see [License File](LICENSE.md) for more information.

## FAQ

### How to get coordinates of a point/cluster:
```PHP
$x = $point[0];
$y = $point[1];

// or

list($x,$y) = $point->getCoordinates();
```

### List all points of a space/cluster:

```PHP
foreach ($cluster as $point) {
printf('[%d,%d]', $point[0], $point[1]);
}
```

### Attach data to a point:

```PHP
$point = $space->addPoint([$x, $y, $z], "user #123");
```

### Retrieve point data:

```PHP
$data = $space[$point]; // e.g. "user #123"
```

### Watch the algorithm run

Each iteration step can be monitored using a callback function passed to `Kmeans\Space::solve`:

```PHP
$clusters = $space->solve(3, function($space, $clusters) {
static $iterations = 0;

printf("Iteration: %d\n", ++$iterations);

foreach ($clusters as $i => $cluster) {
printf("Cluster %d [%d,%d]: %d points\n", $i, $cluster[0], $cluster[1], count($cluster));
}
});
```
13 changes: 8 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
}
],
"require": {
"php": "^7.3|^8.0"
"php": "^7.4|^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3"
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.6",
"phpstan/phpstan": "^1.5",
"mockery/mockery": "^1.4"
},
"autoload": {
"psr-0": {
"KMeans": "src/"
"psr-4": {
"Kmeans\\": "src/"
}
},
"autoload-dev": {
Expand All @@ -33,6 +36,6 @@
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
"test-coverage": "vendor/bin/phpunit --coverage-html build/coverage"
}
}
23 changes: 12 additions & 11 deletions demo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

require "vendor/autoload.php";

// prepare 50 points of 2D space to be clustered
$points = [
$data = [
[80,55],[86,59],[19,85],[41,47],[57,58],
[76,22],[94,60],[13,93],[90,48],[52,54],
[62,46],[88,44],[85,24],[63,14],[51,40],
Expand All @@ -17,24 +16,26 @@
];

// create a 2-dimentions space
$space = new KMeans\Space(2);
$space = new Kmeans\Euclidean\Space(2);

// add points to space
foreach ($points as $i => $coordinates) {
$space->addPoint($coordinates);
}
// prepare the points
$points = new Kmeans\PointCollection($space, array_map([$space, 'makePoint'], $data));

// prepare the algorithm
$algorithm = new Kmeans\Euclidean\Algorithm(new Kmeans\RandomInitialization());

// cluster these 50 points in 3 clusters
$clusters = $space->solve(3);
$clusters = $algorithm->fit($points, 3);

// display the cluster centers and attached points
foreach ($clusters as $num => $cluster) {
$coordinates = $cluster->getCoordinates();
$coordinates = $cluster->getCentroid()->getCoordinates();
assert(is_int($num));
printf(
"Cluster %s [%d,%d]: %d points\n",
"Cluster #%s [%d,%d] has %d points\n",
$num,
$coordinates[0],
$coordinates[1],
count($cluster)
count($cluster->getPoints())
);
}
31 changes: 31 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

# -----------------------------------------------------------------------------
# Code Quality
# -----------------------------------------------------------------------------

qa: phplint phpcs phpstan

QA_PATHS = src/ tests/
QA_STANDARD = psr12

phplint:
find $(QA_PATHS) -name "*.php" -print0 | xargs -0 -n1 -P8 php -l > /dev/null

phpstan:
vendor/bin/phpstan analyse $(QA_PATHS)

phpcs:
vendor/bin/phpcs --standard=$(QA_STANDARD) $(QA_PATHS)

phpcbf:
vendor/bin/phpcbf --standard=$(QA_STANDARD) $(QA_PATHS)

todolist:
git grep -C2 -p -E '[@]todo'

# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------

test:
vendor/bin/phpunit --colors
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parameters:
paths:
- src
- tests
level: 9
61 changes: 29 additions & 32 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
beStrictAboutTestsThatDoNotTestAnything="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
<report>
<clover outputFile="build/logs/clover.xml"/>
<html outputDirectory="build/coverage"/>
<text outputFile="build/coverage.txt"/>
</report>
</coverage>
<testsuites>
<testsuite name="Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
</testsuites>

<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
<report>
<clover outputFile="build/logs/clover.xml"/>
<html outputDirectory="build/coverage"/>
<text outputFile="build/coverage.txt"/>
</report>
</coverage>
</phpunit>
Loading