-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
664 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "tigo/recommendation", | ||
"description": "collaborative filtering recommender systems", | ||
"license": "MIT", | ||
"keywords": [ | ||
"recommendation", | ||
"collaborative filtering", | ||
"euclidean distance", | ||
"recommender system", | ||
"recommendation system", | ||
"recommendation algorithm", | ||
"recommender" | ||
], | ||
"authors": [ | ||
{ | ||
"name": "Tiago A C Pereira", | ||
"email": "[email protected]" | ||
} | ||
], | ||
"require": { | ||
"php": ">=7.0" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Tigo\\Recommendation\\": "src/" | ||
} | ||
}, | ||
"autoload-dev": { | ||
"psr-4": { "Tigo\\Recommendation\\Tests\\": "tests" } | ||
}, | ||
"require-dev": { | ||
"phpunit/phpunit": "^9.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
|
||
<phpunit bootstrap="vendor/autoload.php" | ||
colors="true" | ||
verbose="true" | ||
stopOnFailure="false"> | ||
<testsuites> | ||
<testsuite name="result"> | ||
<directory>tests</directory> | ||
</testsuite> | ||
</testsuites> | ||
</phpunit> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<?php | ||
namespace Tigo\Recommendation\Collaborative; | ||
|
||
use Tigo\Recommendation\Configuration\StandardKey; | ||
use Tigo\Recommendation\Interfaces\CollaborativeInterface; | ||
|
||
/** | ||
* [Base] | ||
* | ||
* @author Tiago A C Pereira <[email protected]> | ||
*/ | ||
abstract class Base extends StandardKey implements CollaborativeInterface | ||
{ | ||
|
||
/** | ||
* User rated product. | ||
* @var array | ||
*/ | ||
protected $product = []; | ||
|
||
/** | ||
* Product rated by other users. | ||
* @var array | ||
*/ | ||
protected $other = []; | ||
|
||
/** | ||
* Get rated product. | ||
* @param array $table | ||
* @param mixed $user | ||
* | ||
* @return [type] | ||
*/ | ||
protected function ratedProduct($table, $user) | ||
{ | ||
foreach($table as $item){ | ||
$item[self::USER_ID] == $user ? $this->product[] = $item : $this->other[] = $item; | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Get filter rating. | ||
* Remove product that the user has rated. | ||
* @param array $data | ||
* | ||
* @return array | ||
*/ | ||
protected function filterRating($data) | ||
{ | ||
$myRank = $data; | ||
$rank = $myRank; | ||
for($i = 0; $i < count($myRank); $i++){ | ||
foreach($this->product as $item){ | ||
if($item[self::PRODUCT_ID] == key($myRank)) | ||
unset($rank[key($myRank)]); // remove product | ||
} | ||
next($myRank); | ||
} | ||
arsort($rank); | ||
return $rank; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
<?php | ||
namespace Tigo\Recommendation\Collaborative; | ||
|
||
use Tigo\Recommendation\Collaborative\Base; | ||
use Tigo\Recommendation\Traits\OperationTrait; | ||
|
||
/** | ||
* Collaborative filtering [recommendation algorithm EuclideanCollaborative]. | ||
* Using the Euclidean distance formula and applying a weighted average. | ||
* | ||
* @author Tiago A C Pereira <[email protected]> | ||
*/ | ||
class EuclideanCollaborative extends Base | ||
{ | ||
|
||
use OperationTrait; | ||
|
||
/** | ||
* Get recommend. | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
public function recommend($table, $user, $score = 0) | ||
{ | ||
$data = $this->average($table, $user, $score); | ||
return $this->filterRating($data); | ||
} | ||
|
||
/** | ||
* Get users who rated the same product. | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
private function userRated($table, $user, $score) | ||
{ | ||
$this->ratedProduct($table, $user); | ||
$rated = []; //get user rating | ||
foreach($this->product as $myProduct){ | ||
foreach($this->other as $item){ | ||
if($myProduct[self::PRODUCT_ID] == $item[self::PRODUCT_ID]){ | ||
if($myProduct[self::SCORE] >= $score && $item[self::SCORE] >= $score){ | ||
if(!in_array($item[self::USER_ID],$rated)) // check if user already exists | ||
$rated[] = $item[self::USER_ID]; //add user | ||
} | ||
} | ||
} | ||
} | ||
return $rated; | ||
} | ||
|
||
/** | ||
* Get operation|using part of the euclidean formula (p-q). | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
private function operation($table, $user, $score) | ||
{ | ||
$rated = $this->userRated($table, $user, $score); | ||
$data = []; | ||
foreach ($this->product as $myProduct){ | ||
for($i = 0; $i < count($rated) ; $i++){ | ||
foreach($this->other as $itemOther){ | ||
if($itemOther[self::USER_ID] == $rated[$i] && | ||
$myProduct[self::PRODUCT_ID] == $itemOther[self::PRODUCT_ID] | ||
&& $myProduct[self::SCORE] >= $score && $itemOther[self::SCORE] >= $score){ | ||
$data[$itemOther[self::USER_ID]][$myProduct[self::PRODUCT_ID]] = abs($itemOther[self::SCORE] - $myProduct[self::SCORE]); | ||
} | ||
} | ||
} | ||
} | ||
return $data; | ||
} | ||
|
||
/** | ||
* Using the metric distance formula and convert value to percentage. | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
private function metricDistance($table, $user, $score) | ||
{ | ||
$data = $this->operation($table, $user, $score); | ||
$element = []; | ||
foreach($data as $item){ | ||
foreach($item as $value){ | ||
if(!isset($element[key($data)])) | ||
$element[key($data)] = 0; | ||
$element[key($data)] += pow($value,2); | ||
} | ||
$similarity = round(sqrt($element[key($data)]),2); //similarity rate | ||
$element[key($data)] = round(1/(1 + $similarity), 2); //convert value | ||
next($data); | ||
} | ||
return $element; | ||
} | ||
|
||
|
||
/** | ||
* Get weighted average. | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
private function average($table, $user, $score) | ||
{ | ||
$metric = $this->metricDistance($table, $user, $score); | ||
$similarity = []; | ||
$element = []; | ||
foreach($metric as $itemMetric){ | ||
foreach($this->other as $itemOther){ | ||
if($itemOther[self::USER_ID] == key($metric) && $itemOther[self::SCORE] >= $score){ | ||
if(!isset($element[$itemOther[self::PRODUCT_ID]])){ | ||
$element[$itemOther[self::PRODUCT_ID]] = 0; | ||
$similarity[$itemOther[self::PRODUCT_ID]] = 0; | ||
} | ||
$element[$itemOther[self::PRODUCT_ID]] += ($itemMetric * $itemOther[self::SCORE]); | ||
$similarity[$itemOther[self::PRODUCT_ID]] += $itemMetric; | ||
} | ||
} | ||
next($metric); | ||
} | ||
return $this->division($element,$similarity); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
<?php | ||
namespace Tigo\Recommendation\Collaborative; | ||
|
||
use Tigo\Recommendation\Collaborative\Base; | ||
|
||
/** | ||
* Collaborative filtering [recommendation algorithm RankingCollaborative]. | ||
* The algorithm checks for similar ratings compared to other users, | ||
* and adds a weight score and generate a rating for each product. | ||
* Example: user1 liked [A, B, C, D] and user2 liked [A, B] | ||
* recommend product [C, D] to user2 (product score = [C = 2 ; D = 2]). | ||
* | ||
* @author Tiago A C Pereira <[email protected]> | ||
*/ | ||
class RankingCollaborative extends Base | ||
{ | ||
|
||
/** | ||
* Get Recommend. | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
public function recommend($table, $user, $score = 0) | ||
{ | ||
$data = $this->addRating($table, $user, $score); | ||
return $this->filterRating($data); | ||
} | ||
|
||
/** | ||
* Find similar users (Add weight score). | ||
* @param array $table | ||
* @param mixed $user | ||
* | ||
* @return array | ||
*/ | ||
private function similarUser($table, $user) | ||
{ | ||
$this->ratedProduct($table, $user); //get [product, other] | ||
$similar = []; //get users with similar tastes | ||
$rank = []; | ||
foreach($this->product as $myProduct){ | ||
foreach($this->other as $item){ | ||
if($myProduct[self::PRODUCT_ID] == $item[self::PRODUCT_ID]){ | ||
if($myProduct[self::SCORE] == $item[self::SCORE]){ | ||
if(!isset($similar[$item[self::USER_ID]])) | ||
$similar[$item[self::USER_ID]] = 0; // | ||
$similar[$item[self::USER_ID]] += 1; //assigning weight | ||
} | ||
} | ||
} | ||
} | ||
return $similar; | ||
} | ||
|
||
|
||
/** | ||
* Add Rating | Add a score (+value) for each recommended product. | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return array | ||
*/ | ||
private function addRating($table, $user, $score) | ||
{ | ||
$similar = $this->similarUser($table, $user); | ||
$rank = []; | ||
foreach($this->other as $item){ | ||
foreach($similar as $value){ | ||
if($item[self::USER_ID] == key($similar) && $item[self::SCORE] > $score){ | ||
if(!isset($rank[$item[self::PRODUCT_ID]]) ) | ||
$rank[$item[self::PRODUCT_ID]] = 0; //assign value for calculation | ||
$rank[$item[self::PRODUCT_ID]] += $value; //add | ||
} | ||
next($similar); | ||
} | ||
reset($similar); | ||
} | ||
return $rank; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
namespace Tigo\Recommendation\Configuration; | ||
|
||
abstract class StandardKey | ||
{ | ||
const SCORE = 'score'; //score | ||
const PRODUCT_ID = 'product_id'; //Foreign key | ||
const USER_ID = 'user_id'; //Foreign key | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
namespace Tigo\Recommendation\Creator; | ||
|
||
use Tigo\Recommendation\Interfaces\CollaborativeInterface; | ||
|
||
abstract class CollaborativeCreator | ||
{ | ||
|
||
/** | ||
* @param CollaborativeInterface $col | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return [type] | ||
*/ | ||
protected abstract function factoryMethod(CollaborativeInterface $col, $table, $user, $score); | ||
|
||
/** | ||
* @param CollaborativeInterface $method | ||
* @param array $table | ||
* @param mixed $user | ||
* @param mixed $score | ||
* | ||
* @return [type] | ||
*/ | ||
public function doFactory($method, $table, $user, $score) | ||
{ | ||
return $this->factoryMethod($method, $table, $user, $score); | ||
} | ||
|
||
} |
Oops, something went wrong.