Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
tigoCaval committed Jan 28, 2021
1 parent f2b6406 commit 732a6a2
Show file tree
Hide file tree
Showing 13 changed files with 664 additions and 0 deletions.
34 changes: 34 additions & 0 deletions composer.json
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"
}
}
12 changes: 12 additions & 0 deletions phpunit.xml
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>
64 changes: 64 additions & 0 deletions src/Collaborative/Base.php
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;
}

}
138 changes: 138 additions & 0 deletions src/Collaborative/EuclideanCollaborative.php
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);
}

}
85 changes: 85 additions & 0 deletions src/Collaborative/RankingCollaborative.php
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;
}

}
9 changes: 9 additions & 0 deletions src/Configuration/StandardKey.php
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
}
32 changes: 32 additions & 0 deletions src/Creator/CollaborativeCreator.php
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);
}

}
Loading

0 comments on commit 732a6a2

Please sign in to comment.