Skip to content

Commit 732a6a2

Browse files
committed
update
1 parent f2b6406 commit 732a6a2

File tree

13 files changed

+664
-0
lines changed

13 files changed

+664
-0
lines changed

composer.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "tigo/recommendation",
3+
"description": "collaborative filtering recommender systems",
4+
"license": "MIT",
5+
"keywords": [
6+
"recommendation",
7+
"collaborative filtering",
8+
"euclidean distance",
9+
"recommender system",
10+
"recommendation system",
11+
"recommendation algorithm",
12+
"recommender"
13+
],
14+
"authors": [
15+
{
16+
"name": "Tiago A C Pereira",
17+
"email": "[email protected]"
18+
}
19+
],
20+
"require": {
21+
"php": ">=7.0"
22+
},
23+
"autoload": {
24+
"psr-4": {
25+
"Tigo\\Recommendation\\": "src/"
26+
}
27+
},
28+
"autoload-dev": {
29+
"psr-4": { "Tigo\\Recommendation\\Tests\\": "tests" }
30+
},
31+
"require-dev": {
32+
"phpunit/phpunit": "^9.5"
33+
}
34+
}

phpunit.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit bootstrap="vendor/autoload.php"
4+
colors="true"
5+
verbose="true"
6+
stopOnFailure="false">
7+
<testsuites>
8+
<testsuite name="result">
9+
<directory>tests</directory>
10+
</testsuite>
11+
</testsuites>
12+
</phpunit>

src/Collaborative/Base.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
namespace Tigo\Recommendation\Collaborative;
3+
4+
use Tigo\Recommendation\Configuration\StandardKey;
5+
use Tigo\Recommendation\Interfaces\CollaborativeInterface;
6+
7+
/**
8+
* [Base]
9+
*
10+
* @author Tiago A C Pereira <[email protected]>
11+
*/
12+
abstract class Base extends StandardKey implements CollaborativeInterface
13+
{
14+
15+
/**
16+
* User rated product.
17+
* @var array
18+
*/
19+
protected $product = [];
20+
21+
/**
22+
* Product rated by other users.
23+
* @var array
24+
*/
25+
protected $other = [];
26+
27+
/**
28+
* Get rated product.
29+
* @param array $table
30+
* @param mixed $user
31+
*
32+
* @return [type]
33+
*/
34+
protected function ratedProduct($table, $user)
35+
{
36+
foreach($table as $item){
37+
$item[self::USER_ID] == $user ? $this->product[] = $item : $this->other[] = $item;
38+
}
39+
}
40+
41+
42+
/**
43+
* Get filter rating.
44+
* Remove product that the user has rated.
45+
* @param array $data
46+
*
47+
* @return array
48+
*/
49+
protected function filterRating($data)
50+
{
51+
$myRank = $data;
52+
$rank = $myRank;
53+
for($i = 0; $i < count($myRank); $i++){
54+
foreach($this->product as $item){
55+
if($item[self::PRODUCT_ID] == key($myRank))
56+
unset($rank[key($myRank)]); // remove product
57+
}
58+
next($myRank);
59+
}
60+
arsort($rank);
61+
return $rank;
62+
}
63+
64+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
namespace Tigo\Recommendation\Collaborative;
3+
4+
use Tigo\Recommendation\Collaborative\Base;
5+
use Tigo\Recommendation\Traits\OperationTrait;
6+
7+
/**
8+
* Collaborative filtering [recommendation algorithm EuclideanCollaborative].
9+
* Using the Euclidean distance formula and applying a weighted average.
10+
*
11+
* @author Tiago A C Pereira <[email protected]>
12+
*/
13+
class EuclideanCollaborative extends Base
14+
{
15+
16+
use OperationTrait;
17+
18+
/**
19+
* Get recommend.
20+
* @param array $table
21+
* @param mixed $user
22+
* @param mixed $score
23+
*
24+
* @return array
25+
*/
26+
public function recommend($table, $user, $score = 0)
27+
{
28+
$data = $this->average($table, $user, $score);
29+
return $this->filterRating($data);
30+
}
31+
32+
/**
33+
* Get users who rated the same product.
34+
* @param array $table
35+
* @param mixed $user
36+
* @param mixed $score
37+
*
38+
* @return array
39+
*/
40+
private function userRated($table, $user, $score)
41+
{
42+
$this->ratedProduct($table, $user);
43+
$rated = []; //get user rating
44+
foreach($this->product as $myProduct){
45+
foreach($this->other as $item){
46+
if($myProduct[self::PRODUCT_ID] == $item[self::PRODUCT_ID]){
47+
if($myProduct[self::SCORE] >= $score && $item[self::SCORE] >= $score){
48+
if(!in_array($item[self::USER_ID],$rated)) // check if user already exists
49+
$rated[] = $item[self::USER_ID]; //add user
50+
}
51+
}
52+
}
53+
}
54+
return $rated;
55+
}
56+
57+
/**
58+
* Get operation|using part of the euclidean formula (p-q).
59+
* @param array $table
60+
* @param mixed $user
61+
* @param mixed $score
62+
*
63+
* @return array
64+
*/
65+
private function operation($table, $user, $score)
66+
{
67+
$rated = $this->userRated($table, $user, $score);
68+
$data = [];
69+
foreach ($this->product as $myProduct){
70+
for($i = 0; $i < count($rated) ; $i++){
71+
foreach($this->other as $itemOther){
72+
if($itemOther[self::USER_ID] == $rated[$i] &&
73+
$myProduct[self::PRODUCT_ID] == $itemOther[self::PRODUCT_ID]
74+
&& $myProduct[self::SCORE] >= $score && $itemOther[self::SCORE] >= $score){
75+
$data[$itemOther[self::USER_ID]][$myProduct[self::PRODUCT_ID]] = abs($itemOther[self::SCORE] - $myProduct[self::SCORE]);
76+
}
77+
}
78+
}
79+
}
80+
return $data;
81+
}
82+
83+
/**
84+
* Using the metric distance formula and convert value to percentage.
85+
* @param array $table
86+
* @param mixed $user
87+
* @param mixed $score
88+
*
89+
* @return array
90+
*/
91+
private function metricDistance($table, $user, $score)
92+
{
93+
$data = $this->operation($table, $user, $score);
94+
$element = [];
95+
foreach($data as $item){
96+
foreach($item as $value){
97+
if(!isset($element[key($data)]))
98+
$element[key($data)] = 0;
99+
$element[key($data)] += pow($value,2);
100+
}
101+
$similarity = round(sqrt($element[key($data)]),2); //similarity rate
102+
$element[key($data)] = round(1/(1 + $similarity), 2); //convert value
103+
next($data);
104+
}
105+
return $element;
106+
}
107+
108+
109+
/**
110+
* Get weighted average.
111+
* @param array $table
112+
* @param mixed $user
113+
* @param mixed $score
114+
*
115+
* @return array
116+
*/
117+
private function average($table, $user, $score)
118+
{
119+
$metric = $this->metricDistance($table, $user, $score);
120+
$similarity = [];
121+
$element = [];
122+
foreach($metric as $itemMetric){
123+
foreach($this->other as $itemOther){
124+
if($itemOther[self::USER_ID] == key($metric) && $itemOther[self::SCORE] >= $score){
125+
if(!isset($element[$itemOther[self::PRODUCT_ID]])){
126+
$element[$itemOther[self::PRODUCT_ID]] = 0;
127+
$similarity[$itemOther[self::PRODUCT_ID]] = 0;
128+
}
129+
$element[$itemOther[self::PRODUCT_ID]] += ($itemMetric * $itemOther[self::SCORE]);
130+
$similarity[$itemOther[self::PRODUCT_ID]] += $itemMetric;
131+
}
132+
}
133+
next($metric);
134+
}
135+
return $this->division($element,$similarity);
136+
}
137+
138+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
namespace Tigo\Recommendation\Collaborative;
3+
4+
use Tigo\Recommendation\Collaborative\Base;
5+
6+
/**
7+
* Collaborative filtering [recommendation algorithm RankingCollaborative].
8+
* The algorithm checks for similar ratings compared to other users,
9+
* and adds a weight score and generate a rating for each product.
10+
* Example: user1 liked [A, B, C, D] and user2 liked [A, B]
11+
* recommend product [C, D] to user2 (product score = [C = 2 ; D = 2]).
12+
*
13+
* @author Tiago A C Pereira <[email protected]>
14+
*/
15+
class RankingCollaborative extends Base
16+
{
17+
18+
/**
19+
* Get Recommend.
20+
* @param array $table
21+
* @param mixed $user
22+
* @param mixed $score
23+
*
24+
* @return array
25+
*/
26+
public function recommend($table, $user, $score = 0)
27+
{
28+
$data = $this->addRating($table, $user, $score);
29+
return $this->filterRating($data);
30+
}
31+
32+
/**
33+
* Find similar users (Add weight score).
34+
* @param array $table
35+
* @param mixed $user
36+
*
37+
* @return array
38+
*/
39+
private function similarUser($table, $user)
40+
{
41+
$this->ratedProduct($table, $user); //get [product, other]
42+
$similar = []; //get users with similar tastes
43+
$rank = [];
44+
foreach($this->product as $myProduct){
45+
foreach($this->other as $item){
46+
if($myProduct[self::PRODUCT_ID] == $item[self::PRODUCT_ID]){
47+
if($myProduct[self::SCORE] == $item[self::SCORE]){
48+
if(!isset($similar[$item[self::USER_ID]]))
49+
$similar[$item[self::USER_ID]] = 0; //
50+
$similar[$item[self::USER_ID]] += 1; //assigning weight
51+
}
52+
}
53+
}
54+
}
55+
return $similar;
56+
}
57+
58+
59+
/**
60+
* Add Rating | Add a score (+value) for each recommended product.
61+
* @param array $table
62+
* @param mixed $user
63+
* @param mixed $score
64+
*
65+
* @return array
66+
*/
67+
private function addRating($table, $user, $score)
68+
{
69+
$similar = $this->similarUser($table, $user);
70+
$rank = [];
71+
foreach($this->other as $item){
72+
foreach($similar as $value){
73+
if($item[self::USER_ID] == key($similar) && $item[self::SCORE] > $score){
74+
if(!isset($rank[$item[self::PRODUCT_ID]]) )
75+
$rank[$item[self::PRODUCT_ID]] = 0; //assign value for calculation
76+
$rank[$item[self::PRODUCT_ID]] += $value; //add
77+
}
78+
next($similar);
79+
}
80+
reset($similar);
81+
}
82+
return $rank;
83+
}
84+
85+
}

src/Configuration/StandardKey.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
namespace Tigo\Recommendation\Configuration;
3+
4+
abstract class StandardKey
5+
{
6+
const SCORE = 'score'; //score
7+
const PRODUCT_ID = 'product_id'; //Foreign key
8+
const USER_ID = 'user_id'; //Foreign key
9+
}

src/Creator/CollaborativeCreator.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
namespace Tigo\Recommendation\Creator;
3+
4+
use Tigo\Recommendation\Interfaces\CollaborativeInterface;
5+
6+
abstract class CollaborativeCreator
7+
{
8+
9+
/**
10+
* @param CollaborativeInterface $col
11+
* @param array $table
12+
* @param mixed $user
13+
* @param mixed $score
14+
*
15+
* @return [type]
16+
*/
17+
protected abstract function factoryMethod(CollaborativeInterface $col, $table, $user, $score);
18+
19+
/**
20+
* @param CollaborativeInterface $method
21+
* @param array $table
22+
* @param mixed $user
23+
* @param mixed $score
24+
*
25+
* @return [type]
26+
*/
27+
public function doFactory($method, $table, $user, $score)
28+
{
29+
return $this->factoryMethod($method, $table, $user, $score);
30+
}
31+
32+
}

0 commit comments

Comments
 (0)