Skip to content

Commit 43b5ab1

Browse files
authored
Refactor recommender service (#4)
* Refactor recommender service * Change register recommender function * Change the tests * Add cron job to install package
1 parent fe3a6c8 commit 43b5ab1

16 files changed

+238
-129
lines changed

.github/workflows/cron.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Check Package
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *' # every day at midnight
6+
7+
jobs:
8+
install:
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
fail-fast: true
12+
matrix:
13+
os: [ ubuntu-latest, windows-latest ]
14+
php: [ 8.1, 8.0, 7.4 ]
15+
stability: [ prefer-stable ]
16+
17+
name: PHP${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
18+
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v2
22+
23+
- name: Setup PHP
24+
uses: shivammathur/setup-php@v2
25+
with:
26+
php-version: ${{ matrix.php }}
27+
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap
28+
29+
- name: Install package
30+
run: composer require phpjuice/opencf

README.md

+17-46
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ composer require phpjuice/opencf
2323

2424
OpenCF Package is designed to be very simple and straightforward to use. All you have to do is:
2525

26-
1. Load training set
27-
2. Select recommendation engine
28-
3. Build a model
29-
4. Predict future ratings based on the training set provided
26+
1. Load a training set (dataset)
27+
3. Predict future ratings using a recommender. (Weighted Slopeone,Cosine, Weighted Cosine)
3028

3129
### Create Recommender Service
3230

@@ -39,34 +37,6 @@ use OpenCF\RecommenderService;
3937
$recommenderService = new RecommenderService($dataset);
4038
```
4139

42-
### Registering a recommendation engine
43-
44-
The recommender service support's 3 recommendation engines (Weighted Slopeone,Cosine, Weighted Cosine).
45-
46-
#### Cosine
47-
48-
```php
49-
// Register a `Cosine` recommendation engine
50-
$recommenderService->registerRecommender('Cosine');
51-
$recommenderService->getRecommender('Cosine'); // OpenCF\Algorithms\Similarity\Cosine::class
52-
```
53-
54-
#### Weighted Cosine
55-
56-
```php
57-
// Register a `Weighted Cosine` recommendation engine
58-
$recommenderService->registerRecommender('WeightedCosine');
59-
$recommenderService->getRecommender('WeightedCosine'); // OpenCF\Algorithms\Similarity\WeightedCosine::class
60-
```
61-
62-
#### Weighted Slopeone
63-
64-
```php
65-
// Register a `Weighted Slopeone` recommendation engine
66-
$recommenderService->registerRecommender('WeightedSlopeone');
67-
$recommenderService->getRecommender('WeightedSlopeone'); // OpenCF\Algorithms\Slopeone\WeightedSlopeone::class
68-
```
69-
7040
### Adding dataset
7141

7242
Adding a dataset to the recommender can be done using the constructor or can be easily done by providing an array of
@@ -103,29 +73,30 @@ $recommenderService->setDataset($dataset);
10373

10474
### Getting Predictions
10575

106-
All you have to do to predict ratings for a new user is to retrieve an engine from the recommender service and
107-
, build the model & run the `predict()` method.
76+
All you have to do to predict ratings for a new user is to retrieve an engine from the recommender service and & run
77+
the `predict()` method.
10878

10979
```php
110-
// Get engine
111-
$weightedSlopeone = $recommenderService->getRecommender('WeightedSlopeone');
112-
113-
// Build model
114-
$weightedSlopeone->buildModel();
115-
116-
// Get predictions
117-
$results = $weightedSlopeone->predict([
80+
// Get a recommender
81+
$recommender = $recommenderService->cosine(); // Cosine recommender
82+
// OR
83+
$recommender = $recommenderService->weightedCosine(); // WeightedCosine recommender
84+
// OR
85+
$recommender = $recommenderService->weightedSlopeone(); // WeightedSlopeone recommender
86+
87+
// Predict future ratings
88+
$results = $recommender->predict([
11889
"squid" => 0.4
11990
]);
12091
```
12192

122-
This should produce the following results
93+
This should produce the following results when using `WeightedSlopeone` recommender
12394

12495
```php
12596
[
126-
"cuttlefish"=>0.25,
127-
"octopus"=>0.23333333333333,
128-
"nautilus"=>0.1
97+
"cuttlefish" => 0.25,
98+
"octopus" => 0.23333333333333,
99+
"nautilus" => 0.1
129100
];
130101
```
131102

composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "phpjuice/opencf",
3-
"description": "OpenCF - an Item-based Collaborative Filtering Engine.",
3+
"description": "PHP implementation of the (Weighted Slopeone,Cosine, Weighted Cosine) rating-based collaborative filtering schemes.",
44
"keywords": [
55
"recommendation",
66
"recommender",
@@ -40,7 +40,7 @@
4040
},
4141
"scripts": {
4242
"test": "vendor/bin/pest --colors=always",
43-
"analyse": "phpstan analyse --ansi --debug",
43+
"analyse": "vendor/bin/phpstan analyse --ansi --debug",
4444
"php-cs-fixer": [
4545
"php-cs-fixer fix src --rules=@PSR2",
4646
"php-cs-fixer fix tests --rules=@PSR2"

phpstan.neon

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
includes:
2-
# -
3-
41
parameters:
2+
53
level: max
4+
65
paths:
76
- src
87
- tests
98

9+
scanFiles:
10+
11+
# Pest handles loading custom helpers only when running tests
12+
# @see https://pestphp.com/docs/helpers#usage
13+
- tests/Pest.php
14+
1015
checkMissingIterableValueType: false
11-
checkGenericClassInNonGenericObjectType: false
1216
reportUnmatchedIgnoredErrors: true
1317

1418
ignoreErrors:
19+
20+
# Pest implicitly binds $this to the current test case
21+
# @see https://pestphp.com/docs/underlying-test-case
22+
-
23+
message: '#^Undefined variable: \$this$#'
24+
paths:
25+
- tests/*
26+
27+
# Pest custom expectations are dynamic and not conducive static analysis
28+
# @see https://pestphp.com/docs/expectations#custom-expectations
29+
-
30+
message: '#Call to an undefined method Pest\\Expectation|Pest\\Support\\Extendable::#'
31+
paths:
32+
- tests/*

src/Algorithms/Slopeone/WeightedSlopeone.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ public function name(): string
1414
return 'WeightedSlopeone';
1515
}
1616

17-
/**
18-
* @inheritdoc
19-
*/
17+
/** @inheritdoc */
2018
public function buildModel(): self
2119
{
2220
$this->similarityFunction = new Similarity($this->vector);

src/Contracts/IRecommenderService.php

+22-5
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,36 @@ interface IRecommenderService
99
/**
1010
* Returns a registered recommender instance.
1111
*
12-
* @param string $name
13-
*
12+
* @param string $recommender
1413
* @return IRecommender
1514
* @throws NotRegisteredRecommenderException
1615
*/
17-
public function getRecommender(string $name): IRecommender;
16+
public function getRecommender(string $recommender): IRecommender;
1817

1918
/**
2019
* Registers a Recommender instance for later use.
2120
*
22-
* @param string $name recommender used to build the model
21+
* @param string $recommender recommender used to build the model
2322
*
2423
* @return $this
2524
*/
26-
public function registerRecommender(string $name): self;
25+
public function registerRecommender(string $recommender): self;
26+
27+
/**
28+
* Return's a weighted slopeone recommender.
29+
* @return IRecommender
30+
*/
31+
public function weightedSlopeone(): IRecommender;
32+
33+
/**
34+
* Return's a weighted cosine recommender.
35+
* @return IRecommender
36+
*/
37+
public function weightedCosine(): IRecommender;
38+
39+
/**
40+
* Return's a cosine recommender.
41+
* @return IRecommender
42+
*/
43+
public function cosine(): IRecommender;
2744
}

src/RecommenderService.php

+62-27
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,27 @@
1010
use OpenCF\Exceptions\EmptyDatasetException;
1111
use OpenCF\Exceptions\NotRegisteredRecommenderException;
1212
use OpenCF\Exceptions\NotSupportedSchemeException;
13+
use ReflectionClass;
14+
use ReflectionException;
1315

1416
class RecommenderService implements IRecommenderService
1517
{
1618
/**
17-
* @var IRecommender[]
19+
* list of registered engines.
20+
*
21+
* @var array<IRecommender>
1822
*/
19-
private array $engines = [];
23+
private array $recommenders = [];
2024

2125
/**
2226
* list of the supported Engines.
2327
*
24-
* @var array
28+
* @var array<string>
2529
*/
26-
private array $supportedEngines = [
27-
'Cosine',
28-
'WeightedCosine',
29-
'WeightedSlopeone',
30+
private array $defaultRecommenders = [
31+
Cosine::class,
32+
WeightedCosine::class,
33+
WeightedSlopeone::class,
3034
];
3135

3236
/**
@@ -42,6 +46,11 @@ class RecommenderService implements IRecommenderService
4246
public function __construct(array $dataset)
4347
{
4448
$this->setDataset($dataset);
49+
50+
// register default recommenders
51+
foreach ($this->defaultRecommenders as $recommender) {
52+
$this->recommenders[$recommender] = new $recommender($this->dataset);
53+
}
4554
}
4655

4756
/**
@@ -62,35 +71,61 @@ public function setDataset(array $dataset = []): self
6271
return $this;
6372
}
6473

65-
public function getRecommender(string $name): IRecommender
74+
public function weightedSlopeone(): IRecommender
6675
{
67-
if (! array_key_exists($name, $this->engines)) {
68-
throw new NotRegisteredRecommenderException(sprintf('The Recommendation engine "%s" is not registered in the Recommender Service',
69-
$name));
76+
if (!in_array(WeightedSlopeone::class, $this->recommenders)) {
77+
$this->registerRecommender(WeightedSlopeone::class);
7078
}
7179

72-
return $this->engines[$name];
80+
return $this->getRecommender(WeightedSlopeone::class);
7381
}
7482

75-
public function registerRecommender(string $name): self
83+
public function registerRecommender(string $recommender): self
7684
{
77-
if (! in_array($name, $this->supportedEngines)) {
78-
throw new NotSupportedSchemeException(sprintf('The Recommendation engine "%s" is not supported yet',
79-
$name));
85+
try {
86+
$rf = new ReflectionClass($recommender);
87+
} catch (ReflectionException $e) {
88+
throw new NotSupportedSchemeException(sprintf('Recommendation engine "%s" must implement "%s" interface',
89+
$recommender, IRecommender::class));
90+
}
91+
92+
if (!$rf->implementsInterface(IRecommender::class)) {
93+
throw new NotSupportedSchemeException(sprintf('Recommendation engine "%s" must implement "%s" interface',
94+
$recommender, IRecommender::class));
8095
}
81-
switch ($name) {
82-
case 'WeightedCosine':
83-
$recommendationEngine = new WeightedCosine($this->dataset);
84-
break;
85-
case 'WeightedSlopeone':
86-
$recommendationEngine = new WeightedSlopeone($this->dataset);
87-
break;
88-
default:
89-
$recommendationEngine = new Cosine($this->dataset);
90-
break;
96+
97+
if (!in_array($recommender, $this->recommenders)) {
98+
$this->recommenders[$recommender] = new $recommender($this->dataset);
9199
}
92-
$this->engines[$recommendationEngine->name()] = $recommendationEngine;
93100

94101
return $this;
95102
}
103+
104+
public function getRecommender(string $recommender): IRecommender
105+
{
106+
if (!array_key_exists($recommender, $this->recommenders)) {
107+
throw new NotRegisteredRecommenderException(sprintf('The Recommendation engine "%s" is not registered in the Recommender Service',
108+
$recommender));
109+
}
110+
111+
return $this->recommenders[$recommender]->buildModel();
112+
}
113+
114+
public function weightedCosine(): IRecommender
115+
{
116+
if (!in_array(WeightedCosine::class, $this->recommenders)) {
117+
$this->registerRecommender(WeightedCosine::class);
118+
}
119+
120+
return $this->getRecommender(WeightedCosine::class);
121+
}
122+
123+
public function cosine(): IRecommender
124+
{
125+
if (!in_array(Cosine::class, $this->recommenders)) {
126+
$this->registerRecommender(Cosine::class);
127+
}
128+
129+
return $this->getRecommender(Cosine::class);
130+
}
96131
}

tests/Algorithms/Similarity/CosineTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use OpenCF\Algorithms\Similarity\Cosine;
4+
45
beforeEach(function () {
56
$this->dataset = [
67
'Batman V Superman' => [
@@ -67,7 +68,7 @@
6768
// $this->dataset
6869
});
6970

70-
test('slopeone scheme predict method', function () {
71+
it('returns correct predictions', function () {
7172
$scheme = new Cosine($this->dataset);
7273
$scheme->buildModel();
7374

tests/Algorithms/Similarity/PredictorTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use OpenCF\Algorithms\Similarity\Cosine;
44
use OpenCF\Algorithms\Similarity\Predictor;
55
use OpenCF\Support\Vector;
6+
67
beforeEach(function () {
78
$this->dataset = [
89
'Item1' => [
@@ -29,7 +30,7 @@
2930
];
3031
});
3132

32-
test('get prediction with cosine', function () {
33+
it('returns correct prediction for a single item', function () {
3334
$cosine = new Cosine($this->dataset);
3435
$cosine->buildModel();
3536
$pred = new Predictor($this->dataset, $cosine->getModel(), new Vector());

0 commit comments

Comments
 (0)