Skip to content
Draft

V3 #34

Show file tree
Hide file tree
Changes from 7 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));
}
});
```
16 changes: 11 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
}
],
"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": "^0.12.97",
"mockery/mockery": "^1.4"
},
"autoload": {
"psr-0": {
"KMeans": "src/"
}
"psr-4": {
"Kmeans\\": "src/"
},
"files": [
"src/math.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
24 changes: 14 additions & 10 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,29 @@
];

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

// add points to space
foreach ($points as $i => $coordinates) {
$space->addPoint($coordinates);
// prepare the points
$points = new Kmeans\PointCollection($space);

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())
);
}
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
# The level 8 is the highest level
level: 8
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