From 339253a82e0dc872539665a6a3bfca725f0b63e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Gazol?= Date: Tue, 5 Jul 2016 11:08:44 +0200 Subject: [PATCH 1/6] FPGrowth integrated --- crossselling.php | 138 ++++++---- fpgrowth/AlgoFPGrowth.php | 365 ++++++++++++++++++++++++++ fpgrowth/FPNode.php | 51 ++++ fpgrowth/FPTree.php | 150 +++++++++++ fpgrowth/Itemset.php | 122 +++++++++ fpgrowth/Itemsets.php | 92 +++++++ fpgrowth/PrestashopWrapper.php | 150 +++++++++++ fpgrowth/TransactionFileGenerator.php | 26 ++ fpgrowth/tests/AlgoFPGrowthTest.php | 87 ++++++ fpgrowth/tests/sample.txt | 7 + fpgrowth/tests/sample_onepath.txt | 5 + 11 files changed, 1140 insertions(+), 53 deletions(-) create mode 100644 fpgrowth/AlgoFPGrowth.php create mode 100644 fpgrowth/FPNode.php create mode 100644 fpgrowth/FPTree.php create mode 100644 fpgrowth/Itemset.php create mode 100644 fpgrowth/Itemsets.php create mode 100644 fpgrowth/PrestashopWrapper.php create mode 100644 fpgrowth/TransactionFileGenerator.php create mode 100644 fpgrowth/tests/AlgoFPGrowthTest.php create mode 100644 fpgrowth/tests/sample.txt create mode 100644 fpgrowth/tests/sample_onepath.txt diff --git a/crossselling.php b/crossselling.php index 32781ab..41a836e 100755 --- a/crossselling.php +++ b/crossselling.php @@ -28,6 +28,8 @@ exit; } +require_once dirname(__FILE__)."/fpgrowth/PrestashopWrapper.php"; + class CrossSelling extends Module { protected $html; @@ -56,8 +58,8 @@ public function install() !$this->registerHook('shoppingCart') || !$this->registerHook('actionOrderStatusPostUpdate') || !Configuration::updateValue('CROSSSELLING_DISPLAY_PRICE', 0) || - !Configuration::updateValue('CROSSSELLING_NBR', 10) - ) { + !Configuration::updateValue('CROSSSELLING_NBR', 10) || + !\fpgrowth\PrestashopWrapper::createTable() ) { return false; } $this->_clearCache('crossselling.tpl'); @@ -65,6 +67,13 @@ public function install() return true; } + public function enable() + { + #$this->injectDummyData(); + return parent::enable(); + + } + public function uninstall() { $this->_clearCache('crossselling.tpl'); @@ -95,6 +104,9 @@ public function getContent() $this->_clearCache('crossselling.tpl'); $this->html .= $this->displayConfirmation($this->l('Settings updated successfully')); } + }elseif (Tools::isSubmit('submitRefresh')) { + $this->processTransactionsDb(); + $this->html .= $this->displayConfirmation($this->l('Rules updated successfully')); } return $this->html.$this->renderForm(); @@ -120,66 +132,57 @@ public function hookHeader() $this->context->controller->addJqueryPlugin(array('scrollTo', 'serialScroll', 'bxslider')); } + + + /** * @param array $products_id an array of product ids * @return array */ protected function getOrderProducts(array $products_id) { - $q_orders = 'SELECT o.id_order - FROM '._DB_PREFIX_.'orders o - LEFT JOIN '._DB_PREFIX_.'order_detail od ON (od.id_order = o.id_order) - WHERE o.valid = 1 AND od.product_id IN ('.implode(',', $products_id).')'; - $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($q_orders); - $final_products_list = array(); + $list_product_ids = join(',', $products_id); + + if (Group::isFeatureActive()) { + $sql_groups_join = ' + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = product_shop.id_category_default + AND cp.id_product = product_shop.id_product) + LEFT JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.`id_category` = cg.`id_category`)'; + $groups = FrontController::getCurrentCustomerGroups(); + $sql_groups_where = 'AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '='.(int)Group::getCurrent()->id); + } - if (count($orders) > 0) { - $list = ''; - foreach ($orders as $order) { - $list .= (int)$order['id_order'].','; - } - $list = rtrim($list, ','); - - $list_product_ids = join(',', $products_id); - - if (Group::isFeatureActive()) { - $sql_groups_join = ' - LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = product_shop.id_category_default - AND cp.id_product = product_shop.id_product) - LEFT JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.`id_category` = cg.`id_category`)'; - $groups = FrontController::getCurrentCustomerGroups(); - $sql_groups_where = 'AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '='.(int)Group::getCurrent()->id); - } - - $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' - SELECT DISTINCT od.product_id, pl.name, pl.description_short, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, - cl.link_rewrite category, p.ean13, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity - FROM '._DB_PREFIX_.'order_detail od - LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = od.product_id) - '.Shop::addSqlAssociation('product', 'p'). - (Combination::isFeatureActive() ? 'LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa - ON (p.`id_product` = pa.`id_product`) - '.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.`default_on` = 1').' - '.Product::sqlStock('p', 'product_attribute_shop', false, $this->context->shop) : Product::sqlStock('p', 'product', false, - $this->context->shop)).' - LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = od.product_id'.Shop::addSqlRestrictionOnLang('pl').') - LEFT JOIN '._DB_PREFIX_.'category_lang cl ON (cl.id_category = product_shop.id_category_default' - .Shop::addSqlRestrictionOnLang('cl').') - LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = od.product_id) - '.(Group::isFeatureActive() ? $sql_groups_join : '').' - WHERE od.id_order IN ('.$list.') + $sqlSelection = ' + SELECT DISTINCT od.product_id, pl.name, pl.description_short, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, + cl.link_rewrite category, p.ean13, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity + FROM '._DB_PREFIX_.'crossselling_pair cp + LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = cp.id_product_main) + '.Shop::addSqlAssociation('product', 'p'). + (Combination::isFeatureActive() ? 'LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (p.`id_product` = pa.`id_product`) + '.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.`default_on` = 1').' + '.Product::sqlStock('p', 'product_attribute_shop', false, $this->context->shop) : Product::sqlStock('p', 'product', false, + $this->context->shop)).' + LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = od.product_id'.Shop::addSqlRestrictionOnLang('pl').') + LEFT JOIN '._DB_PREFIX_.'category_lang cl ON (cl.id_category = product_shop.id_category_default' + .Shop::addSqlRestrictionOnLang('cl').') + LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = od.product_id) + '.(Group::isFeatureActive() ? $sql_groups_join : '').' + WHERE cp.id_product_main in ('.$list_product_ids.') + AND cp.id_product_related NOT IN ('.$list_product_ids.') AND pl.id_lang = '.(int)$this->context->language->id.' AND cl.id_lang = '.(int)$this->context->language->id.' - AND od.product_id NOT IN ('.$list_product_ids.') AND i.cover = 1 AND product_shop.active = 1 '.(Group::isFeatureActive() ? $sql_groups_where : '').' - ORDER BY RAND() - LIMIT '.(int)Configuration::get('CROSSSELLING_NBR')); + ORDER BY cp.support desc + LIMIT '.(int)Configuration::get('CROSSSELLING_NBR'); - $tax_calc = Product::getTaxCalculationMethod(); + $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sqlSelection); + if($order_products != false){ + $tax_calc = Product::getTaxCalculationMethod(); foreach ($order_products as &$order_product) { $order_product['id_product'] = (int)$order_product['product_id']; $order_product['image'] = $this->context->link->getImageLink($order_product['link_rewrite'], @@ -198,7 +201,6 @@ protected function getOrderProducts(array $products_id) } } } - return $final_products_list; } @@ -250,10 +252,10 @@ public function displayProductListReviews($params) */ public function hookProductFooter($params) { - $cache_id = 'crossselling|productfooter|'.(int)$params['product']->id; + $cache_id = 'crossselling|productfooter|'.(int)$params['product']['id']; + if (!$this->isCached('crossselling.tpl', $this->getCacheId($cache_id))) { + $final_products_list = $this->getOrderProducts(array($params['product']['id'])); - if (!$this->isCached('crossselling.tpl', $this->getCacheId($cache_id))) { - $final_products_list = $this->getOrderProducts(array($params['product']->id)); if (count($final_products_list) > 0) { $this->smarty->assign( @@ -313,8 +315,9 @@ public function renderForm() 'title' => $this->l('Save'), ) ), - ); + ); + $helper = new HelperForm(); $helper->show_toolbar = false; $helper->table = $this->table; @@ -332,7 +335,23 @@ public function renderForm() 'id_language' => $this->context->language->id ); - return $helper->generateForm(array($fields_form)); + $forms = $helper->generateForm(array($fields_form)); + + + $fields_form = array( + 'form' => array( + 'legend' => array( + 'title' => $this->l('Association rules') + ), + 'submit' => array( + 'title' => $this->l('Process Transactions'), + ) + ), + + ); + $helper->submit_action = 'submitRefresh'; + $forms .= $helper->generateForm(array($fields_form)); + return $forms; } public function getConfigFieldsValues() @@ -342,4 +361,17 @@ public function getConfigFieldsValues() 'CROSSSELLING_DISPLAY_PRICE' => Tools::getValue('CROSSSELLING_DISPLAY_PRICE', Configuration::get('CROSSSELLING_DISPLAY_PRICE')), ); } + + + /** + * Processes all the carts in search of product association rules + * Saves them into the database + */ + public function processTransactionsDb(){ + $wrapper = new fpgrowth\PrestashopWrapper(); + $levels = $wrapper->getProductAssociationRules(); + $wrapper->saveProductAssociationRules($levels); + } + + } diff --git a/fpgrowth/AlgoFPGrowth.php b/fpgrowth/AlgoFPGrowth.php new file mode 100644 index 0000000..eb79644 --- /dev/null +++ b/fpgrowth/AlgoFPGrowth.php @@ -0,0 +1,365 @@ +startTimestamp = microtime(true); + // number of itemsets found + $this->itemsetCount = 0; + + //initialize tool to record memory usage + $this->memmoryUsed = memory_get_usage(); + $this->patterns = new Itemsets("FREQUENT ITEMSETS"); + + // (1) PREPROCESSING: Initial database scan to determine the frequency of each item + // The frequency is stored in a map: + // key: item value: support + if($mapSupport == null){ + $mapSupport = $this->scanDatabaseToDetermineFrequencyOfSingleItems($input); + } + + // convert the minimum support as percentage to a + // relative minimum support + $this->minSupportRelative = ceil($minsupp * $this->transactionCount); + + // (2) Scan the database again to build the initial FP-Tree + // Before inserting a transaction in the FPTree, we sort the items + // by descending order of support. We ignore items that + // do not have the minimum support. + $tree = new FPTree(); + + // for each line (transaction) until the end of the file + foreach($input as $transactionLine){ + $transaction = []; + // for each item in the transaction add items that have the minimum support + foreach ($transactionLine as $item) { + if ($mapSupport[$item] >= $this->minSupportRelative) { + $transaction[] = $item; + } + } + $this->sortTransaction($transaction, $mapSupport); + + // add the sorted transaction to the fptree. + $tree->addTransaction($transaction); + } + + // We create the header table for the tree using the calculated support of single items + $tree->createHeaderList($mapSupport); + + // (5) We start to mine the FP-Tree by calling the recursive method. + // Initially, the prefix alpha is empty. + // if at least an item is frequent + if (count($tree->headerList) > 0) { + // initialize the buffer for storing the current itemset + $itemsetBuffer = new \SplFixedArray($this::BUFFERS_SIZE); + // and another buffer + $this->fpNodeTempBuffer = new \SplFixedArray($this::BUFFERS_SIZE); + // recursively generate frequent itemsets using the fp-tree + // Note: we assume that the initial FP-Tree has more than one path + // which should generally be the case. + $this->fpgrowth($tree, $itemsetBuffer, 0, $this->transactionCount, $mapSupport); + } + + // record the execution end time + $this->endTime = microtime(true); + // check the memory usage + $this->memmoryUsed = (memory_get_usage() - $this->memmoryUsed) / (1024); + // return the result (if saved to memory) + return $this->patterns; + } + + /** + * Sort the transaction by support or lexical ordering + * @param $transaction + * @param $mapSupport + */ + public function sortTransaction(&$transaction, $mapSupport){ + $sortFunction = function ($id1, $id2) use (&$mapSupport) { + // compare the support + $compare = $mapSupport[$id2] - $mapSupport[$id1]; + // if the same frequency, we check the lexical ordering! + // otherwise we use the support + return ($compare == 0) ? ($id1 - $id2) : $compare; + }; + usort($transaction, $sortFunction); + } + + /** + * Mine an FP-Tree having more than one path. + * @param $tree array the FP-tree + * @param $prefix int the current prefix, named "alpha" + * @param $prefixLength array the frequency of items in the FP-Tree + * @param $prefixSupport int + * @param $mapSupport mixed + */ + private function fpgrowth($tree, $prefix, $prefixLength, $prefixSupport, $mapSupport) + { + // We will check if the FPtree contains a single path + $singlePath = true; + // We will use a variable to keep the support of the single path if there is one + $singlePathSupport = 0; + // This variable is used to count the number of items in the single path + // if there is one + $position = 0; + // if the root has more than one child, than it is not a single path + if (count($tree->root->childs) > 1) { + $singlePath = false; + } else { + // Explore the single path + // if the root has exactly one child, we need to recursively check childs + // of the child to see if they also have one child + $currentNode = $tree->root->childs[0]; + while (true) { + // if the current child has more than one child, it is not a single path! + if (count($currentNode->childs) > 1) { + $singlePath = false; + break; + } + // otherwise, we copy the current item in the buffer and move to the child + // the buffer will be used to store all items in the path + $this->fpNodeTempBuffer[$position] = $currentNode; + $position++; + $singlePathSupport = $currentNode->counter; + // if this node has no child, that means that this is the end of this path + // and it is a single path, so we break + if (count($currentNode->childs) == 0) { + break; + } + $currentNode = $currentNode->childs[0]; + } + } + + // Case 1: the FPtree contains a single path + if ($singlePath && $singlePathSupport >= $this->minSupportRelative) { + // We save the path, because it is a maximal itemset + $this->saveAllCombinationsOfPrefixPath($this->fpNodeTempBuffer, $position, $prefix, $prefixLength); + } else { + // For each frequent item in the header table list of the tree in reverse order. + for ($i = count($tree->headerList) - 1; $i >= 0; $i--) { + // get the item + $item = $tree->headerList[$i]; + // get the item support + $support = $mapSupport[$item]; + // Create Beta by concatening prefix Alpha by adding the current item to alpha + $prefix[$prefixLength] = $item; + // calculate the support of the new prefix beta + $betaSupport = ($prefixSupport < $support) ? $prefixSupport : $support; + // save beta + $this->saveItemset($prefix, $prefixLength + 1, $betaSupport); + // === (A) Construct beta's conditional pattern base === + // It is a subdatabase which consists of the set of prefix paths + // in the FP-tree co-occuring with the prefix pattern. + $prefixPaths = []; + $path = $tree->mapItemNodes[$item]; + // Map to count the support of items in the conditional prefix tree + // Key: item Value: support + $mapSupportBeta = []; + while ($path != null) { + // if the path is not just the root node + if ($path->parent->itemID != -1) { + // create the prefixpath + $prefixPath = []; + // add this node. + $prefixPath[] = $path; // NOTE: we add it just to keep its support, + // actually it should not be part of the prefixPath + $pathCount = $path->counter; + + //Recursively add all the parents of this node. + $parent = $path->parent; + while ($parent->itemID != -1) { + $prefixPath[] = $parent; + // FOR EACH PATTERN WE ALSO UPDATE THE ITEM SUPPORT AT THE SAME TIME + // if the first time we see that node id + if (!isset($mapSupportBeta[$parent->itemID])) { + // just add the path count + $mapSupportBeta[$parent->itemID] = $pathCount; + } else { + // otherwise, make the sum with the value already stored + $mapSupportBeta[$parent->itemID] = $mapSupportBeta[$parent->itemID] + $pathCount; + } + $parent = $parent->parent; + } + // add the path to the list of prefixpaths + $prefixPaths[] = $prefixPath; + } + // We will look for the next prefixpath + $path = $path->nodeLink; + } + + // (B) Construct beta's conditional FP-Tree + // Create the tree. + $treeBeta = new FPTree(); + // Add each prefixpath in the FP-tree. + foreach ($prefixPaths as $prefixPath) { + $treeBeta->addPrefixPath($prefixPath, $mapSupportBeta, $this->minSupportRelative); + } + + // Mine recursively the Beta tree if the root has child(s) + if (count($treeBeta->root->childs) > 0) { + + // Create the header list. + $treeBeta->createHeaderList($mapSupportBeta); + // recursive call + $this->fpgrowth($treeBeta, $prefix, $prefixLength + 1, $betaSupport, $mapSupportBeta); + } + } + } + + } + + /** + * This method saves all combinations of a prefix path if it has enough support + * @param $fpNodeTempBuffer mixed + * @param $position int + * @param $prefix mixed the current prefix + * @param $prefixLength int the current prefix length + * TODO: Check this out with a test + */ + public function saveAllCombinationsOfPrefixPath($fpNodeTempBuffer, $position, + $prefix, $prefixLength){ + $support = 0; + // Generate all subsets of the prefixPath except the empty set + // and output them + // We use bits to generate all subsets. + $i = 1; + $max = pow(2, $position); + for (; $i < $max; $i++) { + // we create a new subset + $newPrefixLength = $prefixLength; + // for each bit + for ($j = 0; $j < $position; $j++) { + // check if the j bit is set to 1 + $isSet = (int)$i & pow(2, $j); + // if yes, add the bit position as an item to the new subset + if ($isSet > 0) { + $prefix[$newPrefixLength++] = $fpNodeTempBuffer[$j]->itemID; + if ($support == 0) { + $support = $fpNodeTempBuffer[$j]->counter; + } + } + } + // save the itemset + $this->saveItemset($prefix, $newPrefixLength, $support); + } + } + + + /** + * This method scans the input database to calculate the support of single items + * @param $input string the path of the input file + * @return mixed a map for storing the support of each item (key: item, value: support) + * TODO: Turn it into SQL query + */ + public function scanDatabaseToDetermineFrequencyOfSingleItems($input) + { + + // a map for storing the support of each item (key: item, value: support) + $mapSupport = []; + foreach($input as $lineSplited){ + // for each item + foreach ($lineSplited as $item) { + // increase the support count of the item + if (!isset($mapSupport["$item"])) { + $mapSupport["$item"] = 1; + } else { + $mapSupport["$item"] += 1; + } + } + // increase the transaction count + $this->transactionCount++; + } + // Unset the file to call __destruct(), closing the file handle. + $file = null; + return $mapSupport; + } + + + /** + * Write a frequent itemset that is found to the output file or + * keep into memory if the user prefer that the result be saved into memory. + * @param $itemset \SplFixedArray + * @param $itemsetLength int + * @param $support int + */ + private function saveItemset($itemset, $itemsetLength, $support) + { + // increase the number of itemsets found for statistics purpose + $this->itemsetCount++; + // create an object Itemset and add it to the set of patterns + // found. + $itemsetArray = array_slice($itemset->toArray(), 0, $itemsetLength); + // sort the itemset so that it is sorted according to lexical ordering before we show it to the user + sort($itemsetArray); + $itemsetObj = new Itemset($itemsetArray); + $itemsetObj->setAbsoluteSupport($support); + $this->patterns->addItemset($itemsetObj, $itemsetLength); + } + + /** + * Print statistics about the algorithm execution to System.out. + */ + public function printStats() + { + print("============= FP-GROWTH 0.96r19 - STATS ============="); + $temps = $this->endTime - $this->startTimestamp; + print("\n Transactions count from database : {$this->transactionCount}"); + print("\n Min support relative: $this->minSupportRelative"); + print("\n Max memory usage Kb: {$this->memmoryUsed}"); + print("\n Frequent itemsets count : {$this->itemsetCount}"); + print("\n Total time ~ {$temps} ms"); + print("\n==================================================="); + } + + +} diff --git a/fpgrowth/FPNode.php b/fpgrowth/FPNode.php new file mode 100644 index 0000000..530070c --- /dev/null +++ b/fpgrowth/FPNode.php @@ -0,0 +1,51 @@ +childs as $child){ + // if the id is the one that we are looking for + if($child->itemID == $id){ + // return that node + return $child; + } + } + // if not found, return null + return null; + } + + /** + * Method for getting a string representation of this tree + * (to be used for debugging purposes). + * @return string a string + */ + public function __toString() { + $strOut = "( {$this->itemID} count={$this->counter})\n"; + $newindent = " "; + foreach($this->childs as $child) { + $strOut .= $newindent . $child; + } + return $strOut; + } + +} diff --git a/fpgrowth/FPTree.php b/fpgrowth/FPTree.php new file mode 100644 index 0000000..0453235 --- /dev/null +++ b/fpgrowth/FPTree.php @@ -0,0 +1,150 @@ +root = new FPNode(); + } + + /** + * Method for adding a transaction to the fp-tree (for the initial construction + * of the FP-Tree). + * @param transaction + */ + public function addTransaction($transaction) { + $currentNode = $this->root; + // For each item in the transaction + foreach($transaction as $item){ + // look if there is a node already in the FP-Tree + $child = $currentNode->getChildWithID($item); + if($child == null){ + // there is no node, we create a new one + $newNode = new FPNode(); + $newNode->itemID = $item; + $newNode->parent = $currentNode; + // we link the new node to its parrent + $currentNode->childs[] = $newNode; + // we take this node as the current node for the next for loop iteration + $currentNode = $newNode; + // We update the header table. + // We check if there is already a node with this id in the header table + $this->fixNodeLinks($item, $newNode); + }else{ + // there is a node already, we update it + $child->counter++; + $currentNode = $child; + } + } + } + + /** + * Method to fix the node link for an item after inserting a new node. + * @param int $item the item of the new node + * @param fpgrowth/FPNode $newNode the new node thas has been inserted. + */ + private function fixNodeLinks($item, $newNode) { + // get the latest node in the tree with this item + if(isset($this->mapItemLastNode[$item])) { + // if not null, then we add the new node to the node link of the last node + $lastNode = $this->mapItemLastNode[$item]; + $lastNode->nodeLink = $newNode; + } + // Finally, we set the new node as the last node + $this->mapItemLastNode[$item] = $newNode; + + if(!isset($this->mapItemNodes[$item])){ // there is not + $this->mapItemNodes[$item] = $newNode; + } + } + + /** + * Method for adding a prefixpath to a fp-tree. + * @param $prefixPath The prefix path + * @param $mapSupportBeta The frequencies of items in the prefixpaths + * @param $relativeMinsupp + */ + function addPrefixPath($prefixPath, $mapSupportBeta, $relativeMinsupp) { + // the first element of the prefix path contains the path support + $pathCount = $prefixPath[0]->counter; + + $currentNode = $this->root; + // For each item in the transaction (in backward order) + // (and we ignore the first element of the prefix path) + for($i = count($prefixPath)-1; $i >=1; $i--){ + $pathItem = $prefixPath[$i]; + // if the item is not frequent we skip it + if($mapSupportBeta[$pathItem->itemID] >= $relativeMinsupp){ + + // look if there is a node already in the FP-Tree + $child = $currentNode->getChildWithID($pathItem->itemID); + if($child == null){ + // there is no node, we create a new one + $newNode = new FPNode(); + $newNode->itemID = $pathItem->itemID; + $newNode->parent = $currentNode; + $newNode->counter = $pathCount; // set its support + $currentNode->childs[] = $newNode; + $currentNode = $newNode; + // We update the header table. + // and the node links + $this->fixNodeLinks($pathItem->itemID, $newNode); + }else{ + // there is a node already, we update it + $child->counter += $pathCount; + $currentNode = $child; + } + } + } + } + + /** + * Method for creating the list of items in the header table, + * in descending order of support. + * @param mixed $mapSupport the frequencies of each item (key: item value: support) + */ + function createHeaderList($mapSupport) { + // create an array to store the header list with + // all the items stored in the map received as parameter + $this->headerList = array_keys($this->mapItemNodes); + + $sortFunction = function ($id1, $id2) use($mapSupport){ + // compare the support + $compare = $mapSupport[$id2] - $mapSupport[$id1]; + // if the same frequency, we check the lexical ordering! + // otherwise we use the support + return ($compare == 0) ? ($id1 - $id2) : $compare; + }; + // sort the header table by decreasing order of support + usort($this->headerList, $sortFunction); + + } + + /** + * Method for getting a string representation of the CP-tree + * (to be used for debugging purposes). + * @return string + */ + public function __toString() { + $temp = "F HeaderList: ". print_r($this->headerList, true) . "\n root: " . $this->root; + return $temp; + } + + +} diff --git a/fpgrowth/Itemset.php b/fpgrowth/Itemset.php new file mode 100644 index 0000000..48b64d7 --- /dev/null +++ b/fpgrowth/Itemset.php @@ -0,0 +1,122 @@ +itemset = $itemset; + } + } + + /** + * Get the items as array + * @return mixed the items + */ + public function getItems() + { + return $this->itemset; + } + + /** + * Get the support of this itemset + */ + public function getAbsoluteSupport() + { + return $this->support; + } + + /** + * Get the size of this itemset + */ + public function size() + { + return count($this->itemset); + } + + /** + * Get the item at a given position in this itemset + * @param $position + * @return mixed + */ + public function get($position) + { + return $this->itemset[$position]; + } + + /** + * Set the support of this itemset + * @param $support int the support + */ + public function setAbsoluteSupport($support) + { + $this->support = $support; + } + + /** + * Increase the support of this itemset by 1 + */ + public function increaseTransactionCount() + { + $this->support++; + } + + /** + * This method return an itemset containing items that are included + * in this itemset and in a given itemset + * @param $itemset2 Itemset the given itemset + * @return Itemset the new itemset + */ + public function intersection($itemset2) + { + $resItemset = new Itemset(); + $resItemset->itemset = $this->intersectTwoSortedArrays($this->getItems(), $itemset2->getItems()); + return $resItemset; + } + + /** + * Intersection of two sorted arrays + * @param $array1 + * @param $array2 + * @return array + */ + public function intersectTwoSortedArrays($array1, $array2) + { + // create a new array having the smallest size between the two arrays + $newArraySize = (count($array1) < count($array2)) ? count($array1) : count($array2); + $newArray = new SplFixedArray($newArraySize); + + $pos1 = 0; + $pos2 = 0; + $posNewArray = 0; + while ($pos1 < count($array1) && $pos2 < count($array2)) { + if ($array1[$pos1] < $array2[$pos2]) { + $pos1++; + } else if ($array2[$pos2] < $array1[$pos1]) { + $pos2++; + } else { // if they are the same + $newArray[$posNewArray] = $array1[$pos1]; + $posNewArray++; + $pos1++; + $pos2++; + } + } + // return the subrange of the new array that is full. + return array_slice($newArray, 0, $posNewArray); + } + + +} diff --git a/fpgrowth/Itemsets.php b/fpgrowth/Itemsets.php new file mode 100644 index 0000000..4580500 --- /dev/null +++ b/fpgrowth/Itemsets.php @@ -0,0 +1,92 @@ +name = $name; + $this->levels[] = []; // We create an empty level 0 by + // default. + } + + /* + */ + public function printItemsets() { + print(" ------- {$this->name} -------\n"); + $patternCount = 0; + $levelCount = 0; + // for each level (a level is a set of itemsets having the same number of items) + foreach ($this->levels as $level) { + // print how many items are contained in this level + print(" L {$levelCount} \n"); + // for each itemset + foreach ($level as $itemset) { + + // print the itemset + print(" pattern {$patternCount}: "); + $itemset->print(); + // print the support of this itemset + print("support : " . $itemset->getAbsoluteSupport() . "\n"); + $patternCount++; + } + $levelCount++; + } + print(" --------------------------------\n"); + } + + /* + * + */ + public function addItemset($itemset, $k) { + while (count($this->levels) <= $k) { + $this->levels[] = []; + } + $this->levels[$k][] = $itemset; + $this->itemsetsCount++; + } + + /* + */ + public function getLevels() { + return $this->levels; + + } + /* + * + */ + public function getItemsetsCount() { + return $this->itemsetsCount; + } + + /* + * + */ + public function setName($newName) { + $this->name = $newName; + } + + /* + * + */ + public function decreaseItemsetCount() { + $this->itemsetsCount--; + } +} diff --git a/fpgrowth/PrestashopWrapper.php b/fpgrowth/PrestashopWrapper.php new file mode 100644 index 0000000..44c772a --- /dev/null +++ b/fpgrowth/PrestashopWrapper.php @@ -0,0 +1,150 @@ +executeS($sql); + $productDict = []; + foreach($rows as $row){ + $productDict["{$row['id_product']}"] = $row['counter']; + } + return $productDict; + } + + /** + * Get the transactions from prestashop db + * @return mixed + */ + public function getTransactionsDb(){ + $sql = " + SELECT group_concat(id_product separator ' ') as transaction + FROM ps_cart_product + GROUP BY id_cart + ORDER BY id_cart DESC + LIMIT " . self::CART_LIMIT; + $rows = \Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + + $transactions = []; + foreach($rows as $row){ + $transactions[] = explode(" ",$row['transaction']); + } + return $transactions; + } + + /** + * Run product association fpgrowth algorithm + * @param $minSupport float minimum support + * @return mixed Levels of association rules found + */ + public function getProductAssociationRules($minSupport = 0.02){ + $algo = new AlgoFPGrowth(); + $patterns = $algo->runAlgorithm($this->getTransactionsDb(), $minSupport,$this->getProductCounters()); + return $patterns->getLevels(); + + } + + /** + * Save the product association rules, we are going to use only level 2 + * @param $levels mixed Association rules derived + */ + public function saveProductAssociationRules($levels){ + if(count($levels[2]) >0){ + #Truncate table + \Db::getInstance()->execute("TRUNCATE TABLE ps_crosseling_pair"); + #Add new rules + $sql = "INSERT INTO ps_crosseling_pair + (`id_product_main`, + `id_product_related`, + `support`) + VALUES "; + foreach($levels[2] as $rule){ + $items = $rule->getItems(); + $sqlValues[] = "({$items[0]},{$items[1]},".$rule->getAbsoluteSupport().")"; + } + $sql .= join(",",$sqlValues); + return \Db::getInstance()->execute($sql); + } + return false; + } + + /** + * Function to create the table needed in the database + */ + public static function createTable(){ + $sql = "CREATE TABLE " . _DB_PREFIX_ . "crosseling_pair ( + `id_crossselling_pair` INT NOT NULL AUTO_INCREMENT, + `id_product_main` INT NOT NULL, + `id_product_related` INT NOT NULL, + `support` INT NULL DEFAULT 0, + PRIMARY KEY (`id_crossselling_pair`), + INDEX `idx_pmain` (`id_product_main` ASC, `support` ASC)) + "; + \Db::getInstance()->execute($sql); + } + + /** + * Function that injects data into test database, do not use in production + */ + private function injectDummyData(){ + $totalCarts = 1000; + $totalProducts = 3; + $idProducts = range(1,2000); + $idCustomers = range(1,10); + #Create cart + for($i=1;$i<$totalCarts;$i++){ + $cart = new Cart(); + $cart->id_shop_group = 1; + $cart->id_shop = 1; + $cart->id_customer = array_rand($idCustomers); + $cart->id_carrier = 2; + $cart->id_address_delivery = 1; + $cart->id_address_invoice = 1; + $cart->id_currency = 1; + $cart->id_lang = 1; + $cart->secure_key = ""; + // Save new cart + $cart->add(); + #Insert products + for($j=0;$j<=$totalProducts;$j++){ + $sql = "INSERT INTO `prestashop`.`ps_cart_product` + (`id_cart`, + `id_product`, + `id_address_delivery`, + `id_shop`, + `id_product_attribute`, + `id_customization`, + `quantity` + ) + VALUES + (".$cart->id.",".array_rand($idProducts).", 1, 1, 0, 0, 1 )"; + \Db::getInstance()->executeS($sql); + } + } + + } +} \ No newline at end of file diff --git a/fpgrowth/TransactionFileGenerator.php b/fpgrowth/TransactionFileGenerator.php new file mode 100644 index 0000000..f251f36 --- /dev/null +++ b/fpgrowth/TransactionFileGenerator.php @@ -0,0 +1,26 @@ + 0) { + return explode(" ", $line); + }else{ + return []; + } + + } + + +} \ No newline at end of file diff --git a/fpgrowth/tests/AlgoFPGrowthTest.php b/fpgrowth/tests/AlgoFPGrowthTest.php new file mode 100644 index 0000000..f69e20c --- /dev/null +++ b/fpgrowth/tests/AlgoFPGrowthTest.php @@ -0,0 +1,87 @@ +object = new fpgrowth\AlgoFPGrowth; + $this->filename = "sample.txt"; + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() + { + } + + /** + * @covers AlgoFPGrowth::runAlgorithm + * @todo Implement testRunAlgorithm(). + */ + public function testRunAlgorithm() + { + $transactionData = new \fpgrowth\TransactionFileGenerator($this->filename); + $minSupport = 0.2; + $patterns = $this->object->runAlgorithm($transactionData, $minSupport); + $levels = $patterns->getLevels(); + $this->assertEquals($levels[3][0]->itemset,['1','2','3']); + + } + + public function testScanDatabaseToDetermineFrequencyOfSingleItems() + { + $transactionData = new \fpgrowth\TransactionFileGenerator($this->filename); + $mapSupport = $this->object->scanDatabaseToDetermineFrequencyOfSingleItems($transactionData ); + $expected = [ + '3' => 4, + '4' => 3, + '1' => 4, + '2' => 4, + '5' => 1 + ]; + $this->assertEquals($mapSupport, $expected); + } + + public function testSortTransaction() + { + $testMap = [ + '3' => 4, + '4' => 3, + '1' => 4, + '2' => 4, + '5' => 1 + ]; + $transaction = ['5','4','1']; + $this->object->sortTransaction($transaction, $testMap); + $this->assertEquals($transaction,['1','4','5']); + } + + public function testOnePathFile() + { + $transactionData = new \fpgrowth\TransactionFileGenerator("sample_onepath.txt"); + $patterns = $this->object->runAlgorithm($transactionData, 0.2); + $levels = $patterns->getLevels(); + $this->assertEquals($levels[3][0]->itemset,['a','b','c']); + } + + + + + +} diff --git a/fpgrowth/tests/sample.txt b/fpgrowth/tests/sample.txt new file mode 100644 index 0000000..cd5016f --- /dev/null +++ b/fpgrowth/tests/sample.txt @@ -0,0 +1,7 @@ +3 4 +3 4 +1 2 +1 2 3 4 +1 2 +1 2 3 +5 \ No newline at end of file diff --git a/fpgrowth/tests/sample_onepath.txt b/fpgrowth/tests/sample_onepath.txt new file mode 100644 index 0000000..b82c4b2 --- /dev/null +++ b/fpgrowth/tests/sample_onepath.txt @@ -0,0 +1,5 @@ +a b c +a b c +a b c +a b c +a b c From 37696cf4690f51bd45364793a57ad813a08993b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Gazol?= Date: Tue, 5 Jul 2016 11:38:34 +0200 Subject: [PATCH 2/6] Fixed table creation and query --- crossselling.php | 19 ++++++++----------- fpgrowth/PrestashopWrapper.php | 12 ++++++------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crossselling.php b/crossselling.php index 41a836e..a1b341a 100755 --- a/crossselling.php +++ b/crossselling.php @@ -69,7 +69,6 @@ public function install() public function enable() { - #$this->injectDummyData(); return parent::enable(); } @@ -154,29 +153,29 @@ protected function getOrderProducts(array $products_id) } $sqlSelection = ' - SELECT DISTINCT od.product_id, pl.name, pl.description_short, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, + SELECT DISTINCT cpa.id_product_related as product_id, pl.name, pl.description_short, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, cl.link_rewrite category, p.ean13, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity - FROM '._DB_PREFIX_.'crossselling_pair cp - LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = cp.id_product_main) + FROM '._DB_PREFIX_.'crossselling_pair cpa + LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = cpa.id_product_related) '.Shop::addSqlAssociation('product', 'p'). (Combination::isFeatureActive() ? 'LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa ON (p.`id_product` = pa.`id_product`) '.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.`default_on` = 1').' '.Product::sqlStock('p', 'product_attribute_shop', false, $this->context->shop) : Product::sqlStock('p', 'product', false, $this->context->shop)).' - LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = od.product_id'.Shop::addSqlRestrictionOnLang('pl').') + LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = cpa.id_product_related'.Shop::addSqlRestrictionOnLang('pl').') LEFT JOIN '._DB_PREFIX_.'category_lang cl ON (cl.id_category = product_shop.id_category_default' .Shop::addSqlRestrictionOnLang('cl').') - LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = od.product_id) + LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = cpa.id_product_related) '.(Group::isFeatureActive() ? $sql_groups_join : '').' - WHERE cp.id_product_main in ('.$list_product_ids.') - AND cp.id_product_related NOT IN ('.$list_product_ids.') + WHERE cpa.id_product_main in ('.$list_product_ids.') + AND cpa.id_product_related NOT IN ('.$list_product_ids.') AND pl.id_lang = '.(int)$this->context->language->id.' AND cl.id_lang = '.(int)$this->context->language->id.' AND i.cover = 1 AND product_shop.active = 1 '.(Group::isFeatureActive() ? $sql_groups_where : '').' - ORDER BY cp.support desc + ORDER BY cpa.support desc LIMIT '.(int)Configuration::get('CROSSSELLING_NBR'); $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sqlSelection); @@ -372,6 +371,4 @@ public function processTransactionsDb(){ $levels = $wrapper->getProductAssociationRules(); $wrapper->saveProductAssociationRules($levels); } - - } diff --git a/fpgrowth/PrestashopWrapper.php b/fpgrowth/PrestashopWrapper.php index 44c772a..21a2e08 100644 --- a/fpgrowth/PrestashopWrapper.php +++ b/fpgrowth/PrestashopWrapper.php @@ -96,7 +96,7 @@ public function saveProductAssociationRules($levels){ * Function to create the table needed in the database */ public static function createTable(){ - $sql = "CREATE TABLE " . _DB_PREFIX_ . "crosseling_pair ( + $sql = "CREATE TABLE " . _DB_PREFIX_ . "crossselling_pair ( `id_crossselling_pair` INT NOT NULL AUTO_INCREMENT, `id_product_main` INT NOT NULL, `id_product_related` INT NOT NULL, @@ -110,14 +110,14 @@ public static function createTable(){ /** * Function that injects data into test database, do not use in production */ - private function injectDummyData(){ - $totalCarts = 1000; + public static function injectDummyCarts(){ + $totalCarts = 100; $totalProducts = 3; - $idProducts = range(1,2000); + $idProducts = range(1,7); $idCustomers = range(1,10); #Create cart for($i=1;$i<$totalCarts;$i++){ - $cart = new Cart(); + $cart = new \Cart(); $cart->id_shop_group = 1; $cart->id_shop = 1; $cart->id_customer = array_rand($idCustomers); @@ -141,7 +141,7 @@ private function injectDummyData(){ `quantity` ) VALUES - (".$cart->id.",".array_rand($idProducts).", 1, 1, 0, 0, 1 )"; + (".$cart->id.",".$idProducts[array_rand($idProducts)].", 1, 1, 0, 0, 1 )"; \Db::getInstance()->executeS($sql); } } From 8de14ecc1a4eb8fc60d04fd33afeb09c6175a5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Gazol?= Date: Tue, 5 Jul 2016 12:06:30 +0200 Subject: [PATCH 3/6] Changed license headers --- crossselling.php | 1 - fpgrowth/AlgoFPGrowth.php | 29 +++++++++++++++++++++++++-- fpgrowth/FPNode.php | 25 +++++++++++++++++++++++ fpgrowth/FPTree.php | 26 ++++++++++++++++++++++++ fpgrowth/Itemset.php | 26 ++++++++++++++++++++++++ fpgrowth/Itemsets.php | 28 ++++++++++++++++++++++++-- fpgrowth/PrestashopWrapper.php | 26 ++++++++++++++++++++++++ fpgrowth/TransactionFileGenerator.php | 27 +++++++++++++++++++++---- fpgrowth/tests/AlgoFPGrowthTest.php | 25 +++++++++++++++++++++++ 9 files changed, 204 insertions(+), 9 deletions(-) diff --git a/crossselling.php b/crossselling.php index a1b341a..d4b1d99 100755 --- a/crossselling.php +++ b/crossselling.php @@ -361,7 +361,6 @@ public function getConfigFieldsValues() ); } - /** * Processes all the carts in search of product association rules * Saves them into the database diff --git a/fpgrowth/AlgoFPGrowth.php b/fpgrowth/AlgoFPGrowth.php index eb79644..4e26d16 100644 --- a/fpgrowth/AlgoFPGrowth.php +++ b/fpgrowth/AlgoFPGrowth.php @@ -1,19 +1,44 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + namespace fpgrowth; + require_once "TransactionFileGenerator.php"; require_once "Itemset.php"; require_once "Itemsets.php"; require_once "FPTree.php"; require_once "FPNode.php"; + /** * This is an implementation of the FPGROWTH algorithm (Han et al., 2004), * derived from the work of Philippe Fournier-Viger http://www.philippe-fournier-viger.com/spmf * FPGrowth is described here: * * Han, J., Pei, J., & Yin, Y. (2000, May). Mining frequent patterns without candidate generation. In ACM SIGMOD Record (Vol. 29, No. 2, pp. 1-12). ACM - * - * @author Jesus Gazol */ class AlgoFPGrowth { diff --git a/fpgrowth/FPNode.php b/fpgrowth/FPNode.php index 530070c..d677989 100644 --- a/fpgrowth/FPNode.php +++ b/fpgrowth/FPNode.php @@ -1,4 +1,29 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + namespace fpgrowth; /** diff --git a/fpgrowth/FPTree.php b/fpgrowth/FPTree.php index 0453235..a098875 100644 --- a/fpgrowth/FPTree.php +++ b/fpgrowth/FPTree.php @@ -1,6 +1,32 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + namespace fpgrowth; + /** * This is an implementation of a FPTree as used by the FPGrowth algorithm. * diff --git a/fpgrowth/Itemset.php b/fpgrowth/Itemset.php index 48b64d7..1939823 100644 --- a/fpgrowth/Itemset.php +++ b/fpgrowth/Itemset.php @@ -1,6 +1,32 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + namespace fpgrowth; + /** * This class represents an itemset (a set of items) implemented as an array of integers with * a variable to store the support count of the itemset. diff --git a/fpgrowth/Itemsets.php b/fpgrowth/Itemsets.php index 4580500..48a35ca 100644 --- a/fpgrowth/Itemsets.php +++ b/fpgrowth/Itemsets.php @@ -1,12 +1,36 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + namespace fpgrowth; + /** * This class represents a set of itemsets, where an itemset is an array of integers * with an associated support count. Itemsets are ordered by size. For * example, level 1 means itemsets of size 1 (that contains 1 item). -* - * @author Jesus Gazol */ class Itemsets{ /** We store the itemsets in a list named "levels". diff --git a/fpgrowth/PrestashopWrapper.php b/fpgrowth/PrestashopWrapper.php index 21a2e08..49c9700 100644 --- a/fpgrowth/PrestashopWrapper.php +++ b/fpgrowth/PrestashopWrapper.php @@ -1,5 +1,31 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + namespace fpgrowth; + require_once "AlgoFPGrowth.php"; /** diff --git a/fpgrowth/TransactionFileGenerator.php b/fpgrowth/TransactionFileGenerator.php index f251f36..93499b6 100644 --- a/fpgrowth/TransactionFileGenerator.php +++ b/fpgrowth/TransactionFileGenerator.php @@ -1,8 +1,27 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA */ namespace fpgrowth; @@ -13,7 +32,7 @@ class TransactionFileGenerator extends \SplFileObject function current() { - $line = trim(parent::current()); // TODO: Change the autogenerated stub + $line = trim(parent::current()); if (strlen($line) > 0) { return explode(" ", $line); }else{ diff --git a/fpgrowth/tests/AlgoFPGrowthTest.php b/fpgrowth/tests/AlgoFPGrowthTest.php index f69e20c..fab3cc7 100644 --- a/fpgrowth/tests/AlgoFPGrowthTest.php +++ b/fpgrowth/tests/AlgoFPGrowthTest.php @@ -1,4 +1,29 @@ + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + require_once("../AlgoFPGrowth.php"); /** * Generated by PHPUnit_SkeletonGenerator on 2016-07-01 at 12:42:07. From 9be8448f3e84b02f10857140d7d9e6e027c53d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Gazol?= Date: Tue, 5 Jul 2016 12:10:04 +0200 Subject: [PATCH 4/6] Reformatted to comply with standards --- fpgrowth/AlgoFPGrowth.php | 18 +- fpgrowth/FPTree.php | 275 +++++++++++++------------- fpgrowth/Itemset.php | 8 +- fpgrowth/Itemsets.php | 159 ++++++++------- fpgrowth/PrestashopWrapper.php | 50 ++--- fpgrowth/TransactionFileGenerator.php | 8 +- fpgrowth/tests/AlgoFPGrowthTest.php | 20 +- 7 files changed, 280 insertions(+), 258 deletions(-) diff --git a/fpgrowth/AlgoFPGrowth.php b/fpgrowth/AlgoFPGrowth.php index 4e26d16..637bdb4 100644 --- a/fpgrowth/AlgoFPGrowth.php +++ b/fpgrowth/AlgoFPGrowth.php @@ -18,9 +18,9 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ @@ -90,7 +90,7 @@ public function runAlgorithm($input, $minsupp, $mapSupport = null) // (1) PREPROCESSING: Initial database scan to determine the frequency of each item // The frequency is stored in a map: // key: item value: support - if($mapSupport == null){ + if ($mapSupport == null) { $mapSupport = $this->scanDatabaseToDetermineFrequencyOfSingleItems($input); } @@ -105,7 +105,7 @@ public function runAlgorithm($input, $minsupp, $mapSupport = null) $tree = new FPTree(); // for each line (transaction) until the end of the file - foreach($input as $transactionLine){ + foreach ($input as $transactionLine) { $transaction = []; // for each item in the transaction add items that have the minimum support foreach ($transactionLine as $item) { @@ -149,7 +149,8 @@ public function runAlgorithm($input, $minsupp, $mapSupport = null) * @param $transaction * @param $mapSupport */ - public function sortTransaction(&$transaction, $mapSupport){ + public function sortTransaction(&$transaction, $mapSupport) + { $sortFunction = function ($id1, $id2) use (&$mapSupport) { // compare the support $compare = $mapSupport[$id2] - $mapSupport[$id1]; @@ -292,7 +293,8 @@ private function fpgrowth($tree, $prefix, $prefixLength, $prefixSupport, $mapSup * TODO: Check this out with a test */ public function saveAllCombinationsOfPrefixPath($fpNodeTempBuffer, $position, - $prefix, $prefixLength){ + $prefix, $prefixLength) + { $support = 0; // Generate all subsets of the prefixPath except the empty set // and output them @@ -331,7 +333,7 @@ public function scanDatabaseToDetermineFrequencyOfSingleItems($input) // a map for storing the support of each item (key: item, value: support) $mapSupport = []; - foreach($input as $lineSplited){ + foreach ($input as $lineSplited) { // for each item foreach ($lineSplited as $item) { // increase the support count of the item diff --git a/fpgrowth/FPTree.php b/fpgrowth/FPTree.php index a098875..074ed87 100644 --- a/fpgrowth/FPTree.php +++ b/fpgrowth/FPTree.php @@ -18,9 +18,9 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ @@ -31,146 +31,153 @@ * This is an implementation of a FPTree as used by the FPGrowth algorithm. * */ -class FPTree { - // List of items in the header table - public $headerList = []; +class FPTree +{ + // List of items in the header table + public $headerList = []; - // List of pairs (item, frequency) of the header table - public $mapItemNodes = []; + // List of pairs (item, frequency) of the header table + public $mapItemNodes = []; - // Map that indicates the last node for each item using the node links - // key: item value: an fp tree node - public $mapItemLastNode = []; + // Map that indicates the last node for each item using the node links + // key: item value: an fp tree node + public $mapItemLastNode = []; - // root of the tree - public $root ; // null node + // root of the tree + public $root; // null node - public function __construct(){ + public function __construct() + { $this->root = new FPNode(); } /** - * Method for adding a transaction to the fp-tree (for the initial construction - * of the FP-Tree). - * @param transaction - */ - public function addTransaction($transaction) { - $currentNode = $this->root; - // For each item in the transaction - foreach($transaction as $item){ - // look if there is a node already in the FP-Tree - $child = $currentNode->getChildWithID($item); - if($child == null){ - // there is no node, we create a new one - $newNode = new FPNode(); - $newNode->itemID = $item; - $newNode->parent = $currentNode; - // we link the new node to its parrent - $currentNode->childs[] = $newNode; - // we take this node as the current node for the next for loop iteration - $currentNode = $newNode; - // We update the header table. - // We check if there is already a node with this id in the header table - $this->fixNodeLinks($item, $newNode); - }else{ - // there is a node already, we update it - $child->counter++; - $currentNode = $child; - } - } - } - - /** - * Method to fix the node link for an item after inserting a new node. - * @param int $item the item of the new node - * @param fpgrowth/FPNode $newNode the new node thas has been inserted. - */ - private function fixNodeLinks($item, $newNode) { - // get the latest node in the tree with this item - if(isset($this->mapItemLastNode[$item])) { - // if not null, then we add the new node to the node link of the last node + * Method for adding a transaction to the fp-tree (for the initial construction + * of the FP-Tree). + * @param transaction + */ + public function addTransaction($transaction) + { + $currentNode = $this->root; + // For each item in the transaction + foreach ($transaction as $item) { + // look if there is a node already in the FP-Tree + $child = $currentNode->getChildWithID($item); + if ($child == null) { + // there is no node, we create a new one + $newNode = new FPNode(); + $newNode->itemID = $item; + $newNode->parent = $currentNode; + // we link the new node to its parrent + $currentNode->childs[] = $newNode; + // we take this node as the current node for the next for loop iteration + $currentNode = $newNode; + // We update the header table. + // We check if there is already a node with this id in the header table + $this->fixNodeLinks($item, $newNode); + } else { + // there is a node already, we update it + $child->counter++; + $currentNode = $child; + } + } + } + + /** + * Method to fix the node link for an item after inserting a new node. + * @param int $item the item of the new node + * @param fpgrowth /FPNode $newNode the new node thas has been inserted. + */ + private function fixNodeLinks($item, $newNode) + { + // get the latest node in the tree with this item + if (isset($this->mapItemLastNode[$item])) { + // if not null, then we add the new node to the node link of the last node $lastNode = $this->mapItemLastNode[$item]; - $lastNode->nodeLink = $newNode; - } - // Finally, we set the new node as the last node - $this->mapItemLastNode[$item] = $newNode; - - if(!isset($this->mapItemNodes[$item])){ // there is not - $this->mapItemNodes[$item] = $newNode; - } - } - - /** - * Method for adding a prefixpath to a fp-tree. - * @param $prefixPath The prefix path - * @param $mapSupportBeta The frequencies of items in the prefixpaths - * @param $relativeMinsupp - */ - function addPrefixPath($prefixPath, $mapSupportBeta, $relativeMinsupp) { - // the first element of the prefix path contains the path support - $pathCount = $prefixPath[0]->counter; - - $currentNode = $this->root; - // For each item in the transaction (in backward order) - // (and we ignore the first element of the prefix path) - for($i = count($prefixPath)-1; $i >=1; $i--){ - $pathItem = $prefixPath[$i]; - // if the item is not frequent we skip it - if($mapSupportBeta[$pathItem->itemID] >= $relativeMinsupp){ - - // look if there is a node already in the FP-Tree - $child = $currentNode->getChildWithID($pathItem->itemID); - if($child == null){ - // there is no node, we create a new one - $newNode = new FPNode(); - $newNode->itemID = $pathItem->itemID; - $newNode->parent = $currentNode; - $newNode->counter = $pathCount; // set its support - $currentNode->childs[] = $newNode; - $currentNode = $newNode; - // We update the header table. - // and the node links - $this->fixNodeLinks($pathItem->itemID, $newNode); - }else{ - // there is a node already, we update it - $child->counter += $pathCount; - $currentNode = $child; - } - } - } - } - - /** - * Method for creating the list of items in the header table, - * in descending order of support. - * @param mixed $mapSupport the frequencies of each item (key: item value: support) - */ - function createHeaderList($mapSupport) { - // create an array to store the header list with - // all the items stored in the map received as parameter - $this->headerList = array_keys($this->mapItemNodes); - - $sortFunction = function ($id1, $id2) use($mapSupport){ - // compare the support - $compare = $mapSupport[$id2] - $mapSupport[$id1]; - // if the same frequency, we check the lexical ordering! - // otherwise we use the support - return ($compare == 0) ? ($id1 - $id2) : $compare; - }; - // sort the header table by decreasing order of support - usort($this->headerList, $sortFunction); - - } - - /** - * Method for getting a string representation of the CP-tree - * (to be used for debugging purposes). - * @return string - */ - public function __toString() { - $temp = "F HeaderList: ". print_r($this->headerList, true) . "\n root: " . $this->root; - return $temp; - } + $lastNode->nodeLink = $newNode; + } + // Finally, we set the new node as the last node + $this->mapItemLastNode[$item] = $newNode; + + if (!isset($this->mapItemNodes[$item])) { // there is not + $this->mapItemNodes[$item] = $newNode; + } + } + + /** + * Method for adding a prefixpath to a fp-tree. + * @param $prefixPath The prefix path + * @param $mapSupportBeta The frequencies of items in the prefixpaths + * @param $relativeMinsupp + */ + function addPrefixPath($prefixPath, $mapSupportBeta, $relativeMinsupp) + { + // the first element of the prefix path contains the path support + $pathCount = $prefixPath[0]->counter; + + $currentNode = $this->root; + // For each item in the transaction (in backward order) + // (and we ignore the first element of the prefix path) + for ($i = count($prefixPath) - 1; $i >= 1; $i--) { + $pathItem = $prefixPath[$i]; + // if the item is not frequent we skip it + if ($mapSupportBeta[$pathItem->itemID] >= $relativeMinsupp) { + + // look if there is a node already in the FP-Tree + $child = $currentNode->getChildWithID($pathItem->itemID); + if ($child == null) { + // there is no node, we create a new one + $newNode = new FPNode(); + $newNode->itemID = $pathItem->itemID; + $newNode->parent = $currentNode; + $newNode->counter = $pathCount; // set its support + $currentNode->childs[] = $newNode; + $currentNode = $newNode; + // We update the header table. + // and the node links + $this->fixNodeLinks($pathItem->itemID, $newNode); + } else { + // there is a node already, we update it + $child->counter += $pathCount; + $currentNode = $child; + } + } + } + } + + /** + * Method for creating the list of items in the header table, + * in descending order of support. + * @param mixed $mapSupport the frequencies of each item (key: item value: support) + */ + function createHeaderList($mapSupport) + { + // create an array to store the header list with + // all the items stored in the map received as parameter + $this->headerList = array_keys($this->mapItemNodes); + + $sortFunction = function ($id1, $id2) use ($mapSupport) { + // compare the support + $compare = $mapSupport[$id2] - $mapSupport[$id1]; + // if the same frequency, we check the lexical ordering! + // otherwise we use the support + return ($compare == 0) ? ($id1 - $id2) : $compare; + }; + // sort the header table by decreasing order of support + usort($this->headerList, $sortFunction); + + } + + /** + * Method for getting a string representation of the CP-tree + * (to be used for debugging purposes). + * @return string + */ + public function __toString() + { + $temp = "F HeaderList: " . print_r($this->headerList, true) . "\n root: " . $this->root; + return $temp; + } } diff --git a/fpgrowth/Itemset.php b/fpgrowth/Itemset.php index 1939823..949b167 100644 --- a/fpgrowth/Itemset.php +++ b/fpgrowth/Itemset.php @@ -18,9 +18,9 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ @@ -43,7 +43,7 @@ class Itemset public function __construct($itemset = false) { - if($itemset != false){ + if ($itemset != false) { $this->itemset = $itemset; } } diff --git a/fpgrowth/Itemsets.php b/fpgrowth/Itemsets.php index 48a35ca..29f6fb5 100644 --- a/fpgrowth/Itemsets.php +++ b/fpgrowth/Itemsets.php @@ -18,9 +18,9 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ @@ -32,85 +32,94 @@ * with an associated support count. Itemsets are ordered by size. For * example, level 1 means itemsets of size 1 (that contains 1 item). */ -class Itemsets{ - /** We store the itemsets in a list named "levels". - Position i in "levels" contains the list of itemsets of size i */ - private $levels = []; - /** the total number of itemsets **/ - private $itemsetsCount = 0; - /** a name that we give to these itemsets (e.g. "frequent itemsets") */ - private $name; +class Itemsets +{ + /** We store the itemsets in a list named "levels". + * Position i in "levels" contains the list of itemsets of size i */ + private $levels = []; + /** the total number of itemsets **/ + private $itemsetsCount = 0; + /** a name that we give to these itemsets (e.g. "frequent itemsets") */ + private $name; - /** - * Constructor - * @param string $name the name of these itemsets - */ - public function __construct($name) { - $this->name = $name; - $this->levels[] = []; // We create an empty level 0 by - // default. - } + /** + * Constructor + * @param string $name the name of these itemsets + */ + public function __construct($name) + { + $this->name = $name; + $this->levels[] = []; // We create an empty level 0 by + // default. + } - /* - */ - public function printItemsets() { - print(" ------- {$this->name} -------\n"); - $patternCount = 0; - $levelCount = 0; - // for each level (a level is a set of itemsets having the same number of items) - foreach ($this->levels as $level) { - // print how many items are contained in this level - print(" L {$levelCount} \n"); - // for each itemset - foreach ($level as $itemset) { + /* + */ + public function printItemsets() + { + print(" ------- {$this->name} -------\n"); + $patternCount = 0; + $levelCount = 0; + // for each level (a level is a set of itemsets having the same number of items) + foreach ($this->levels as $level) { + // print how many items are contained in this level + print(" L {$levelCount} \n"); + // for each itemset + foreach ($level as $itemset) { - // print the itemset - print(" pattern {$patternCount}: "); - $itemset->print(); - // print the support of this itemset - print("support : " . $itemset->getAbsoluteSupport() . "\n"); - $patternCount++; - } - $levelCount++; - } - print(" --------------------------------\n"); - } + // print the itemset + print(" pattern {$patternCount}: "); + $itemset->print(); + // print the support of this itemset + print("support : " . $itemset->getAbsoluteSupport() . "\n"); + $patternCount++; + } + $levelCount++; + } + print(" --------------------------------\n"); + } - /* - * - */ - public function addItemset($itemset, $k) { - while (count($this->levels) <= $k) { - $this->levels[] = []; - } - $this->levels[$k][] = $itemset; - $this->itemsetsCount++; - } + /* + * + */ + public function addItemset($itemset, $k) + { + while (count($this->levels) <= $k) { + $this->levels[] = []; + } + $this->levels[$k][] = $itemset; + $this->itemsetsCount++; + } - /* - */ - public function getLevels() { - return $this->levels; + /* + */ + public function getLevels() + { + return $this->levels; } - /* - * - */ - public function getItemsetsCount() { - return $this->itemsetsCount; - } - /* - * - */ - public function setName($newName) { - $this->name = $newName; - } + /* + * + */ + public function getItemsetsCount() + { + return $this->itemsetsCount; + } - /* - * - */ - public function decreaseItemsetCount() { - $this->itemsetsCount--; - } + /* + * + */ + public function setName($newName) + { + $this->name = $newName; + } + + /* + * + */ + public function decreaseItemsetCount() + { + $this->itemsetsCount--; + } } diff --git a/fpgrowth/PrestashopWrapper.php b/fpgrowth/PrestashopWrapper.php index 49c9700..f373c38 100644 --- a/fpgrowth/PrestashopWrapper.php +++ b/fpgrowth/PrestashopWrapper.php @@ -18,9 +18,9 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ @@ -47,7 +47,8 @@ class PrestashopWrapper * Get the product counters from prestashop db * @return array */ - public function getProductCounters(){ + public function getProductCounters() + { $sql = " SELECT id_product, count(0) as counter FROM ps_cart_product @@ -56,7 +57,7 @@ public function getProductCounters(){ LIMIT " . self::TRANSACTION_LIMIT; $rows = \Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); $productDict = []; - foreach($rows as $row){ + foreach ($rows as $row) { $productDict["{$row['id_product']}"] = $row['counter']; } return $productDict; @@ -66,7 +67,8 @@ public function getProductCounters(){ * Get the transactions from prestashop db * @return mixed */ - public function getTransactionsDb(){ + public function getTransactionsDb() + { $sql = " SELECT group_concat(id_product separator ' ') as transaction FROM ps_cart_product @@ -76,8 +78,8 @@ public function getTransactionsDb(){ $rows = \Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); $transactions = []; - foreach($rows as $row){ - $transactions[] = explode(" ",$row['transaction']); + foreach ($rows as $row) { + $transactions[] = explode(" ", $row['transaction']); } return $transactions; } @@ -87,9 +89,10 @@ public function getTransactionsDb(){ * @param $minSupport float minimum support * @return mixed Levels of association rules found */ - public function getProductAssociationRules($minSupport = 0.02){ + public function getProductAssociationRules($minSupport = 0.02) + { $algo = new AlgoFPGrowth(); - $patterns = $algo->runAlgorithm($this->getTransactionsDb(), $minSupport,$this->getProductCounters()); + $patterns = $algo->runAlgorithm($this->getTransactionsDb(), $minSupport, $this->getProductCounters()); return $patterns->getLevels(); } @@ -98,8 +101,9 @@ public function getProductAssociationRules($minSupport = 0.02){ * Save the product association rules, we are going to use only level 2 * @param $levels mixed Association rules derived */ - public function saveProductAssociationRules($levels){ - if(count($levels[2]) >0){ + public function saveProductAssociationRules($levels) + { + if (count($levels[2]) > 0) { #Truncate table \Db::getInstance()->execute("TRUNCATE TABLE ps_crosseling_pair"); #Add new rules @@ -108,11 +112,11 @@ public function saveProductAssociationRules($levels){ `id_product_related`, `support`) VALUES "; - foreach($levels[2] as $rule){ + foreach ($levels[2] as $rule) { $items = $rule->getItems(); - $sqlValues[] = "({$items[0]},{$items[1]},".$rule->getAbsoluteSupport().")"; + $sqlValues[] = "({$items[0]},{$items[1]}," . $rule->getAbsoluteSupport() . ")"; } - $sql .= join(",",$sqlValues); + $sql .= join(",", $sqlValues); return \Db::getInstance()->execute($sql); } return false; @@ -121,7 +125,8 @@ public function saveProductAssociationRules($levels){ /** * Function to create the table needed in the database */ - public static function createTable(){ + public static function createTable() + { $sql = "CREATE TABLE " . _DB_PREFIX_ . "crossselling_pair ( `id_crossselling_pair` INT NOT NULL AUTO_INCREMENT, `id_product_main` INT NOT NULL, @@ -136,13 +141,14 @@ public static function createTable(){ /** * Function that injects data into test database, do not use in production */ - public static function injectDummyCarts(){ + public static function injectDummyCarts() + { $totalCarts = 100; $totalProducts = 3; - $idProducts = range(1,7); - $idCustomers = range(1,10); + $idProducts = range(1, 7); + $idCustomers = range(1, 10); #Create cart - for($i=1;$i<$totalCarts;$i++){ + for ($i = 1; $i < $totalCarts; $i++) { $cart = new \Cart(); $cart->id_shop_group = 1; $cart->id_shop = 1; @@ -156,7 +162,7 @@ public static function injectDummyCarts(){ // Save new cart $cart->add(); #Insert products - for($j=0;$j<=$totalProducts;$j++){ + for ($j = 0; $j <= $totalProducts; $j++) { $sql = "INSERT INTO `prestashop`.`ps_cart_product` (`id_cart`, `id_product`, @@ -167,7 +173,7 @@ public static function injectDummyCarts(){ `quantity` ) VALUES - (".$cart->id.",".$idProducts[array_rand($idProducts)].", 1, 1, 0, 0, 1 )"; + (" . $cart->id . "," . $idProducts[array_rand($idProducts)] . ", 1, 1, 0, 0, 1 )"; \Db::getInstance()->executeS($sql); } } diff --git a/fpgrowth/TransactionFileGenerator.php b/fpgrowth/TransactionFileGenerator.php index 93499b6..330d602 100644 --- a/fpgrowth/TransactionFileGenerator.php +++ b/fpgrowth/TransactionFileGenerator.php @@ -18,9 +18,9 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ @@ -35,7 +35,7 @@ function current() $line = trim(parent::current()); if (strlen($line) > 0) { return explode(" ", $line); - }else{ + } else { return []; } diff --git a/fpgrowth/tests/AlgoFPGrowthTest.php b/fpgrowth/tests/AlgoFPGrowthTest.php index fab3cc7..a728af6 100644 --- a/fpgrowth/tests/AlgoFPGrowthTest.php +++ b/fpgrowth/tests/AlgoFPGrowthTest.php @@ -18,13 +18,14 @@ * versions in the future. If you wish to customize PrestaShop for your * needs please refer to http://www.prestashop.com for more information. * - * @author Jesus Gazol - * @copyright 2007-2016 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * @author Jesus Gazol + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA */ require_once("../AlgoFPGrowth.php"); + /** * Generated by PHPUnit_SkeletonGenerator on 2016-07-01 at 12:42:07. */ @@ -65,14 +66,14 @@ public function testRunAlgorithm() $minSupport = 0.2; $patterns = $this->object->runAlgorithm($transactionData, $minSupport); $levels = $patterns->getLevels(); - $this->assertEquals($levels[3][0]->itemset,['1','2','3']); + $this->assertEquals($levels[3][0]->itemset, ['1', '2', '3']); } public function testScanDatabaseToDetermineFrequencyOfSingleItems() { $transactionData = new \fpgrowth\TransactionFileGenerator($this->filename); - $mapSupport = $this->object->scanDatabaseToDetermineFrequencyOfSingleItems($transactionData ); + $mapSupport = $this->object->scanDatabaseToDetermineFrequencyOfSingleItems($transactionData); $expected = [ '3' => 4, '4' => 3, @@ -92,9 +93,9 @@ public function testSortTransaction() '2' => 4, '5' => 1 ]; - $transaction = ['5','4','1']; + $transaction = ['5', '4', '1']; $this->object->sortTransaction($transaction, $testMap); - $this->assertEquals($transaction,['1','4','5']); + $this->assertEquals($transaction, ['1', '4', '5']); } public function testOnePathFile() @@ -102,11 +103,8 @@ public function testOnePathFile() $transactionData = new \fpgrowth\TransactionFileGenerator("sample_onepath.txt"); $patterns = $this->object->runAlgorithm($transactionData, 0.2); $levels = $patterns->getLevels(); - $this->assertEquals($levels[3][0]->itemset,['a','b','c']); + $this->assertEquals($levels[3][0]->itemset, ['a', 'b', 'c']); } - - - } From e490ba56dfe2af17279b8b4554cf5844d34e24cf Mon Sep 17 00:00:00 2001 From: Jesus Gazol Date: Tue, 5 Jul 2016 12:31:30 +0200 Subject: [PATCH 5/6] MO : FPGrowth integrated #NM-793 --- crossselling.php | 134 +++++---- fpgrowth/AlgoFPGrowth.php | 392 ++++++++++++++++++++++++++ fpgrowth/FPNode.php | 76 +++++ fpgrowth/FPTree.php | 183 ++++++++++++ fpgrowth/Itemset.php | 148 ++++++++++ fpgrowth/Itemsets.php | 125 ++++++++ fpgrowth/PrestashopWrapper.php | 182 ++++++++++++ fpgrowth/TransactionFileGenerator.php | 45 +++ fpgrowth/tests/AlgoFPGrowthTest.php | 110 ++++++++ fpgrowth/tests/sample.txt | 7 + fpgrowth/tests/sample_onepath.txt | 5 + 11 files changed, 1354 insertions(+), 53 deletions(-) create mode 100644 fpgrowth/AlgoFPGrowth.php create mode 100644 fpgrowth/FPNode.php create mode 100644 fpgrowth/FPTree.php create mode 100644 fpgrowth/Itemset.php create mode 100644 fpgrowth/Itemsets.php create mode 100644 fpgrowth/PrestashopWrapper.php create mode 100644 fpgrowth/TransactionFileGenerator.php create mode 100644 fpgrowth/tests/AlgoFPGrowthTest.php create mode 100644 fpgrowth/tests/sample.txt create mode 100644 fpgrowth/tests/sample_onepath.txt diff --git a/crossselling.php b/crossselling.php index 32781ab..d4b1d99 100755 --- a/crossselling.php +++ b/crossselling.php @@ -28,6 +28,8 @@ exit; } +require_once dirname(__FILE__)."/fpgrowth/PrestashopWrapper.php"; + class CrossSelling extends Module { protected $html; @@ -56,8 +58,8 @@ public function install() !$this->registerHook('shoppingCart') || !$this->registerHook('actionOrderStatusPostUpdate') || !Configuration::updateValue('CROSSSELLING_DISPLAY_PRICE', 0) || - !Configuration::updateValue('CROSSSELLING_NBR', 10) - ) { + !Configuration::updateValue('CROSSSELLING_NBR', 10) || + !\fpgrowth\PrestashopWrapper::createTable() ) { return false; } $this->_clearCache('crossselling.tpl'); @@ -65,6 +67,12 @@ public function install() return true; } + public function enable() + { + return parent::enable(); + + } + public function uninstall() { $this->_clearCache('crossselling.tpl'); @@ -95,6 +103,9 @@ public function getContent() $this->_clearCache('crossselling.tpl'); $this->html .= $this->displayConfirmation($this->l('Settings updated successfully')); } + }elseif (Tools::isSubmit('submitRefresh')) { + $this->processTransactionsDb(); + $this->html .= $this->displayConfirmation($this->l('Rules updated successfully')); } return $this->html.$this->renderForm(); @@ -120,66 +131,57 @@ public function hookHeader() $this->context->controller->addJqueryPlugin(array('scrollTo', 'serialScroll', 'bxslider')); } + + + /** * @param array $products_id an array of product ids * @return array */ protected function getOrderProducts(array $products_id) { - $q_orders = 'SELECT o.id_order - FROM '._DB_PREFIX_.'orders o - LEFT JOIN '._DB_PREFIX_.'order_detail od ON (od.id_order = o.id_order) - WHERE o.valid = 1 AND od.product_id IN ('.implode(',', $products_id).')'; - $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($q_orders); - $final_products_list = array(); + $list_product_ids = join(',', $products_id); + + if (Group::isFeatureActive()) { + $sql_groups_join = ' + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = product_shop.id_category_default + AND cp.id_product = product_shop.id_product) + LEFT JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.`id_category` = cg.`id_category`)'; + $groups = FrontController::getCurrentCustomerGroups(); + $sql_groups_where = 'AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '='.(int)Group::getCurrent()->id); + } - if (count($orders) > 0) { - $list = ''; - foreach ($orders as $order) { - $list .= (int)$order['id_order'].','; - } - $list = rtrim($list, ','); - - $list_product_ids = join(',', $products_id); - - if (Group::isFeatureActive()) { - $sql_groups_join = ' - LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = product_shop.id_category_default - AND cp.id_product = product_shop.id_product) - LEFT JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.`id_category` = cg.`id_category`)'; - $groups = FrontController::getCurrentCustomerGroups(); - $sql_groups_where = 'AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '='.(int)Group::getCurrent()->id); - } - - $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' - SELECT DISTINCT od.product_id, pl.name, pl.description_short, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, - cl.link_rewrite category, p.ean13, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity - FROM '._DB_PREFIX_.'order_detail od - LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = od.product_id) - '.Shop::addSqlAssociation('product', 'p'). - (Combination::isFeatureActive() ? 'LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa - ON (p.`id_product` = pa.`id_product`) - '.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.`default_on` = 1').' - '.Product::sqlStock('p', 'product_attribute_shop', false, $this->context->shop) : Product::sqlStock('p', 'product', false, - $this->context->shop)).' - LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = od.product_id'.Shop::addSqlRestrictionOnLang('pl').') - LEFT JOIN '._DB_PREFIX_.'category_lang cl ON (cl.id_category = product_shop.id_category_default' - .Shop::addSqlRestrictionOnLang('cl').') - LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = od.product_id) - '.(Group::isFeatureActive() ? $sql_groups_join : '').' - WHERE od.id_order IN ('.$list.') + $sqlSelection = ' + SELECT DISTINCT cpa.id_product_related as product_id, pl.name, pl.description_short, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, + cl.link_rewrite category, p.ean13, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity + FROM '._DB_PREFIX_.'crossselling_pair cpa + LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = cpa.id_product_related) + '.Shop::addSqlAssociation('product', 'p'). + (Combination::isFeatureActive() ? 'LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (p.`id_product` = pa.`id_product`) + '.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.`default_on` = 1').' + '.Product::sqlStock('p', 'product_attribute_shop', false, $this->context->shop) : Product::sqlStock('p', 'product', false, + $this->context->shop)).' + LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = cpa.id_product_related'.Shop::addSqlRestrictionOnLang('pl').') + LEFT JOIN '._DB_PREFIX_.'category_lang cl ON (cl.id_category = product_shop.id_category_default' + .Shop::addSqlRestrictionOnLang('cl').') + LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = cpa.id_product_related) + '.(Group::isFeatureActive() ? $sql_groups_join : '').' + WHERE cpa.id_product_main in ('.$list_product_ids.') + AND cpa.id_product_related NOT IN ('.$list_product_ids.') AND pl.id_lang = '.(int)$this->context->language->id.' AND cl.id_lang = '.(int)$this->context->language->id.' - AND od.product_id NOT IN ('.$list_product_ids.') AND i.cover = 1 AND product_shop.active = 1 '.(Group::isFeatureActive() ? $sql_groups_where : '').' - ORDER BY RAND() - LIMIT '.(int)Configuration::get('CROSSSELLING_NBR')); + ORDER BY cpa.support desc + LIMIT '.(int)Configuration::get('CROSSSELLING_NBR'); - $tax_calc = Product::getTaxCalculationMethod(); + $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sqlSelection); + if($order_products != false){ + $tax_calc = Product::getTaxCalculationMethod(); foreach ($order_products as &$order_product) { $order_product['id_product'] = (int)$order_product['product_id']; $order_product['image'] = $this->context->link->getImageLink($order_product['link_rewrite'], @@ -198,7 +200,6 @@ protected function getOrderProducts(array $products_id) } } } - return $final_products_list; } @@ -250,10 +251,10 @@ public function displayProductListReviews($params) */ public function hookProductFooter($params) { - $cache_id = 'crossselling|productfooter|'.(int)$params['product']->id; + $cache_id = 'crossselling|productfooter|'.(int)$params['product']['id']; + if (!$this->isCached('crossselling.tpl', $this->getCacheId($cache_id))) { + $final_products_list = $this->getOrderProducts(array($params['product']['id'])); - if (!$this->isCached('crossselling.tpl', $this->getCacheId($cache_id))) { - $final_products_list = $this->getOrderProducts(array($params['product']->id)); if (count($final_products_list) > 0) { $this->smarty->assign( @@ -313,8 +314,9 @@ public function renderForm() 'title' => $this->l('Save'), ) ), - ); + ); + $helper = new HelperForm(); $helper->show_toolbar = false; $helper->table = $this->table; @@ -332,7 +334,23 @@ public function renderForm() 'id_language' => $this->context->language->id ); - return $helper->generateForm(array($fields_form)); + $forms = $helper->generateForm(array($fields_form)); + + + $fields_form = array( + 'form' => array( + 'legend' => array( + 'title' => $this->l('Association rules') + ), + 'submit' => array( + 'title' => $this->l('Process Transactions'), + ) + ), + + ); + $helper->submit_action = 'submitRefresh'; + $forms .= $helper->generateForm(array($fields_form)); + return $forms; } public function getConfigFieldsValues() @@ -342,4 +360,14 @@ public function getConfigFieldsValues() 'CROSSSELLING_DISPLAY_PRICE' => Tools::getValue('CROSSSELLING_DISPLAY_PRICE', Configuration::get('CROSSSELLING_DISPLAY_PRICE')), ); } + + /** + * Processes all the carts in search of product association rules + * Saves them into the database + */ + public function processTransactionsDb(){ + $wrapper = new fpgrowth\PrestashopWrapper(); + $levels = $wrapper->getProductAssociationRules(); + $wrapper->saveProductAssociationRules($levels); + } } diff --git a/fpgrowth/AlgoFPGrowth.php b/fpgrowth/AlgoFPGrowth.php new file mode 100644 index 0000000..637bdb4 --- /dev/null +++ b/fpgrowth/AlgoFPGrowth.php @@ -0,0 +1,392 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + +require_once "TransactionFileGenerator.php"; +require_once "Itemset.php"; +require_once "Itemsets.php"; +require_once "FPTree.php"; +require_once "FPNode.php"; + + +/** + * This is an implementation of the FPGROWTH algorithm (Han et al., 2004), + * derived from the work of Philippe Fournier-Viger http://www.philippe-fournier-viger.com/spmf + * FPGrowth is described here: + * + * Han, J., Pei, J., & Yin, Y. (2000, May). Mining frequent patterns without candidate generation. In ACM SIGMOD Record (Vol. 29, No. 2, pp. 1-12). ACM + */ +class AlgoFPGrowth +{ + // This variable is used to determine the size of buffers to store itemsets. + // A value of 50 is enough because it allows up to 2^50 patterns! + const BUFFERS_SIZE = 50; + + // for statistics + public $startTimestamp;// start time of the latest execution + public $endTime; // end time of the latest execution + public $transactionCount = 0; // transaction count in the database + public $itemsetCount; // number of freq. itemsets found + public $memmoryUsed = 0; + + // parameter + public $minSupportRelative;// the relative minimum support + + // The patterns that are found + // (if the user want to keep them into memory) + public $patterns = null; + // buffer for storing the current itemset that is mined when performing mining + // the idea is to always reuse the same buffer to reduce memory usage. + public $itemsetBuffer = null; + // another buffer for storing fpnodes in a single path of the tree + public $fpNodeTempBuffer = null; + // This buffer is used to store an itemset that will be written to file + // so that the algorithm can sort the itemset before it is output to file + // (when the user choose to output result to file). + public $itemsetOutputBuffer = null; + + /** + * Method to run the FPGRowth algorithm. + * @param mixed $input an array like db of transactions. + * @param float $minsupp the minimum support threshold. + * @param mixed $mapSupport + * @return fpgrowth /Itemsets result if no output file path is provided. + */ + public function runAlgorithm($input, $minsupp, $mapSupport = null) + { + // record start time + $this->startTimestamp = microtime(true); + // number of itemsets found + $this->itemsetCount = 0; + + //initialize tool to record memory usage + $this->memmoryUsed = memory_get_usage(); + $this->patterns = new Itemsets("FREQUENT ITEMSETS"); + + // (1) PREPROCESSING: Initial database scan to determine the frequency of each item + // The frequency is stored in a map: + // key: item value: support + if ($mapSupport == null) { + $mapSupport = $this->scanDatabaseToDetermineFrequencyOfSingleItems($input); + } + + // convert the minimum support as percentage to a + // relative minimum support + $this->minSupportRelative = ceil($minsupp * $this->transactionCount); + + // (2) Scan the database again to build the initial FP-Tree + // Before inserting a transaction in the FPTree, we sort the items + // by descending order of support. We ignore items that + // do not have the minimum support. + $tree = new FPTree(); + + // for each line (transaction) until the end of the file + foreach ($input as $transactionLine) { + $transaction = []; + // for each item in the transaction add items that have the minimum support + foreach ($transactionLine as $item) { + if ($mapSupport[$item] >= $this->minSupportRelative) { + $transaction[] = $item; + } + } + $this->sortTransaction($transaction, $mapSupport); + + // add the sorted transaction to the fptree. + $tree->addTransaction($transaction); + } + + // We create the header table for the tree using the calculated support of single items + $tree->createHeaderList($mapSupport); + + // (5) We start to mine the FP-Tree by calling the recursive method. + // Initially, the prefix alpha is empty. + // if at least an item is frequent + if (count($tree->headerList) > 0) { + // initialize the buffer for storing the current itemset + $itemsetBuffer = new \SplFixedArray($this::BUFFERS_SIZE); + // and another buffer + $this->fpNodeTempBuffer = new \SplFixedArray($this::BUFFERS_SIZE); + // recursively generate frequent itemsets using the fp-tree + // Note: we assume that the initial FP-Tree has more than one path + // which should generally be the case. + $this->fpgrowth($tree, $itemsetBuffer, 0, $this->transactionCount, $mapSupport); + } + + // record the execution end time + $this->endTime = microtime(true); + // check the memory usage + $this->memmoryUsed = (memory_get_usage() - $this->memmoryUsed) / (1024); + // return the result (if saved to memory) + return $this->patterns; + } + + /** + * Sort the transaction by support or lexical ordering + * @param $transaction + * @param $mapSupport + */ + public function sortTransaction(&$transaction, $mapSupport) + { + $sortFunction = function ($id1, $id2) use (&$mapSupport) { + // compare the support + $compare = $mapSupport[$id2] - $mapSupport[$id1]; + // if the same frequency, we check the lexical ordering! + // otherwise we use the support + return ($compare == 0) ? ($id1 - $id2) : $compare; + }; + usort($transaction, $sortFunction); + } + + /** + * Mine an FP-Tree having more than one path. + * @param $tree array the FP-tree + * @param $prefix int the current prefix, named "alpha" + * @param $prefixLength array the frequency of items in the FP-Tree + * @param $prefixSupport int + * @param $mapSupport mixed + */ + private function fpgrowth($tree, $prefix, $prefixLength, $prefixSupport, $mapSupport) + { + // We will check if the FPtree contains a single path + $singlePath = true; + // We will use a variable to keep the support of the single path if there is one + $singlePathSupport = 0; + // This variable is used to count the number of items in the single path + // if there is one + $position = 0; + // if the root has more than one child, than it is not a single path + if (count($tree->root->childs) > 1) { + $singlePath = false; + } else { + // Explore the single path + // if the root has exactly one child, we need to recursively check childs + // of the child to see if they also have one child + $currentNode = $tree->root->childs[0]; + while (true) { + // if the current child has more than one child, it is not a single path! + if (count($currentNode->childs) > 1) { + $singlePath = false; + break; + } + // otherwise, we copy the current item in the buffer and move to the child + // the buffer will be used to store all items in the path + $this->fpNodeTempBuffer[$position] = $currentNode; + $position++; + $singlePathSupport = $currentNode->counter; + // if this node has no child, that means that this is the end of this path + // and it is a single path, so we break + if (count($currentNode->childs) == 0) { + break; + } + $currentNode = $currentNode->childs[0]; + } + } + + // Case 1: the FPtree contains a single path + if ($singlePath && $singlePathSupport >= $this->minSupportRelative) { + // We save the path, because it is a maximal itemset + $this->saveAllCombinationsOfPrefixPath($this->fpNodeTempBuffer, $position, $prefix, $prefixLength); + } else { + // For each frequent item in the header table list of the tree in reverse order. + for ($i = count($tree->headerList) - 1; $i >= 0; $i--) { + // get the item + $item = $tree->headerList[$i]; + // get the item support + $support = $mapSupport[$item]; + // Create Beta by concatening prefix Alpha by adding the current item to alpha + $prefix[$prefixLength] = $item; + // calculate the support of the new prefix beta + $betaSupport = ($prefixSupport < $support) ? $prefixSupport : $support; + // save beta + $this->saveItemset($prefix, $prefixLength + 1, $betaSupport); + // === (A) Construct beta's conditional pattern base === + // It is a subdatabase which consists of the set of prefix paths + // in the FP-tree co-occuring with the prefix pattern. + $prefixPaths = []; + $path = $tree->mapItemNodes[$item]; + // Map to count the support of items in the conditional prefix tree + // Key: item Value: support + $mapSupportBeta = []; + while ($path != null) { + // if the path is not just the root node + if ($path->parent->itemID != -1) { + // create the prefixpath + $prefixPath = []; + // add this node. + $prefixPath[] = $path; // NOTE: we add it just to keep its support, + // actually it should not be part of the prefixPath + $pathCount = $path->counter; + + //Recursively add all the parents of this node. + $parent = $path->parent; + while ($parent->itemID != -1) { + $prefixPath[] = $parent; + // FOR EACH PATTERN WE ALSO UPDATE THE ITEM SUPPORT AT THE SAME TIME + // if the first time we see that node id + if (!isset($mapSupportBeta[$parent->itemID])) { + // just add the path count + $mapSupportBeta[$parent->itemID] = $pathCount; + } else { + // otherwise, make the sum with the value already stored + $mapSupportBeta[$parent->itemID] = $mapSupportBeta[$parent->itemID] + $pathCount; + } + $parent = $parent->parent; + } + // add the path to the list of prefixpaths + $prefixPaths[] = $prefixPath; + } + // We will look for the next prefixpath + $path = $path->nodeLink; + } + + // (B) Construct beta's conditional FP-Tree + // Create the tree. + $treeBeta = new FPTree(); + // Add each prefixpath in the FP-tree. + foreach ($prefixPaths as $prefixPath) { + $treeBeta->addPrefixPath($prefixPath, $mapSupportBeta, $this->minSupportRelative); + } + + // Mine recursively the Beta tree if the root has child(s) + if (count($treeBeta->root->childs) > 0) { + + // Create the header list. + $treeBeta->createHeaderList($mapSupportBeta); + // recursive call + $this->fpgrowth($treeBeta, $prefix, $prefixLength + 1, $betaSupport, $mapSupportBeta); + } + } + } + + } + + /** + * This method saves all combinations of a prefix path if it has enough support + * @param $fpNodeTempBuffer mixed + * @param $position int + * @param $prefix mixed the current prefix + * @param $prefixLength int the current prefix length + * TODO: Check this out with a test + */ + public function saveAllCombinationsOfPrefixPath($fpNodeTempBuffer, $position, + $prefix, $prefixLength) + { + $support = 0; + // Generate all subsets of the prefixPath except the empty set + // and output them + // We use bits to generate all subsets. + $i = 1; + $max = pow(2, $position); + for (; $i < $max; $i++) { + // we create a new subset + $newPrefixLength = $prefixLength; + // for each bit + for ($j = 0; $j < $position; $j++) { + // check if the j bit is set to 1 + $isSet = (int)$i & pow(2, $j); + // if yes, add the bit position as an item to the new subset + if ($isSet > 0) { + $prefix[$newPrefixLength++] = $fpNodeTempBuffer[$j]->itemID; + if ($support == 0) { + $support = $fpNodeTempBuffer[$j]->counter; + } + } + } + // save the itemset + $this->saveItemset($prefix, $newPrefixLength, $support); + } + } + + + /** + * This method scans the input database to calculate the support of single items + * @param $input string the path of the input file + * @return mixed a map for storing the support of each item (key: item, value: support) + * TODO: Turn it into SQL query + */ + public function scanDatabaseToDetermineFrequencyOfSingleItems($input) + { + + // a map for storing the support of each item (key: item, value: support) + $mapSupport = []; + foreach ($input as $lineSplited) { + // for each item + foreach ($lineSplited as $item) { + // increase the support count of the item + if (!isset($mapSupport["$item"])) { + $mapSupport["$item"] = 1; + } else { + $mapSupport["$item"] += 1; + } + } + // increase the transaction count + $this->transactionCount++; + } + // Unset the file to call __destruct(), closing the file handle. + $file = null; + return $mapSupport; + } + + + /** + * Write a frequent itemset that is found to the output file or + * keep into memory if the user prefer that the result be saved into memory. + * @param $itemset \SplFixedArray + * @param $itemsetLength int + * @param $support int + */ + private function saveItemset($itemset, $itemsetLength, $support) + { + // increase the number of itemsets found for statistics purpose + $this->itemsetCount++; + // create an object Itemset and add it to the set of patterns + // found. + $itemsetArray = array_slice($itemset->toArray(), 0, $itemsetLength); + // sort the itemset so that it is sorted according to lexical ordering before we show it to the user + sort($itemsetArray); + $itemsetObj = new Itemset($itemsetArray); + $itemsetObj->setAbsoluteSupport($support); + $this->patterns->addItemset($itemsetObj, $itemsetLength); + } + + /** + * Print statistics about the algorithm execution to System.out. + */ + public function printStats() + { + print("============= FP-GROWTH 0.96r19 - STATS ============="); + $temps = $this->endTime - $this->startTimestamp; + print("\n Transactions count from database : {$this->transactionCount}"); + print("\n Min support relative: $this->minSupportRelative"); + print("\n Max memory usage Kb: {$this->memmoryUsed}"); + print("\n Frequent itemsets count : {$this->itemsetCount}"); + print("\n Total time ~ {$temps} ms"); + print("\n==================================================="); + } + + +} diff --git a/fpgrowth/FPNode.php b/fpgrowth/FPNode.php new file mode 100644 index 0000000..d677989 --- /dev/null +++ b/fpgrowth/FPNode.php @@ -0,0 +1,76 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + +/** + * This is an implementation of a FPTree node as used by the FPGrowth algorithm. + * + */ +class FPNode { + public $itemID = -1; // item id + public $counter = 1; // frequency counter (a.k.a. support) + + // the parent node of that node or null if it is the root + public $parent = null; + // the child nodes of that node + public $childs = []; + public $nodeLink = null; // link to next node with the same item id (for the header table). + + /** + * Return the immediate child of this node having a given ID. + * If there is no such child, return null; + * @param int $id + * @return mixed|null + */ + function getChildWithID($id) { + // for each child node + foreach($this->childs as $child){ + // if the id is the one that we are looking for + if($child->itemID == $id){ + // return that node + return $child; + } + } + // if not found, return null + return null; + } + + /** + * Method for getting a string representation of this tree + * (to be used for debugging purposes). + * @return string a string + */ + public function __toString() { + $strOut = "( {$this->itemID} count={$this->counter})\n"; + $newindent = " "; + foreach($this->childs as $child) { + $strOut .= $newindent . $child; + } + return $strOut; + } + +} diff --git a/fpgrowth/FPTree.php b/fpgrowth/FPTree.php new file mode 100644 index 0000000..074ed87 --- /dev/null +++ b/fpgrowth/FPTree.php @@ -0,0 +1,183 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + + +/** + * This is an implementation of a FPTree as used by the FPGrowth algorithm. + * + */ +class FPTree +{ + // List of items in the header table + public $headerList = []; + + // List of pairs (item, frequency) of the header table + public $mapItemNodes = []; + + // Map that indicates the last node for each item using the node links + // key: item value: an fp tree node + public $mapItemLastNode = []; + + // root of the tree + public $root; // null node + + public function __construct() + { + $this->root = new FPNode(); + } + + /** + * Method for adding a transaction to the fp-tree (for the initial construction + * of the FP-Tree). + * @param transaction + */ + public function addTransaction($transaction) + { + $currentNode = $this->root; + // For each item in the transaction + foreach ($transaction as $item) { + // look if there is a node already in the FP-Tree + $child = $currentNode->getChildWithID($item); + if ($child == null) { + // there is no node, we create a new one + $newNode = new FPNode(); + $newNode->itemID = $item; + $newNode->parent = $currentNode; + // we link the new node to its parrent + $currentNode->childs[] = $newNode; + // we take this node as the current node for the next for loop iteration + $currentNode = $newNode; + // We update the header table. + // We check if there is already a node with this id in the header table + $this->fixNodeLinks($item, $newNode); + } else { + // there is a node already, we update it + $child->counter++; + $currentNode = $child; + } + } + } + + /** + * Method to fix the node link for an item after inserting a new node. + * @param int $item the item of the new node + * @param fpgrowth /FPNode $newNode the new node thas has been inserted. + */ + private function fixNodeLinks($item, $newNode) + { + // get the latest node in the tree with this item + if (isset($this->mapItemLastNode[$item])) { + // if not null, then we add the new node to the node link of the last node + $lastNode = $this->mapItemLastNode[$item]; + $lastNode->nodeLink = $newNode; + } + // Finally, we set the new node as the last node + $this->mapItemLastNode[$item] = $newNode; + + if (!isset($this->mapItemNodes[$item])) { // there is not + $this->mapItemNodes[$item] = $newNode; + } + } + + /** + * Method for adding a prefixpath to a fp-tree. + * @param $prefixPath The prefix path + * @param $mapSupportBeta The frequencies of items in the prefixpaths + * @param $relativeMinsupp + */ + function addPrefixPath($prefixPath, $mapSupportBeta, $relativeMinsupp) + { + // the first element of the prefix path contains the path support + $pathCount = $prefixPath[0]->counter; + + $currentNode = $this->root; + // For each item in the transaction (in backward order) + // (and we ignore the first element of the prefix path) + for ($i = count($prefixPath) - 1; $i >= 1; $i--) { + $pathItem = $prefixPath[$i]; + // if the item is not frequent we skip it + if ($mapSupportBeta[$pathItem->itemID] >= $relativeMinsupp) { + + // look if there is a node already in the FP-Tree + $child = $currentNode->getChildWithID($pathItem->itemID); + if ($child == null) { + // there is no node, we create a new one + $newNode = new FPNode(); + $newNode->itemID = $pathItem->itemID; + $newNode->parent = $currentNode; + $newNode->counter = $pathCount; // set its support + $currentNode->childs[] = $newNode; + $currentNode = $newNode; + // We update the header table. + // and the node links + $this->fixNodeLinks($pathItem->itemID, $newNode); + } else { + // there is a node already, we update it + $child->counter += $pathCount; + $currentNode = $child; + } + } + } + } + + /** + * Method for creating the list of items in the header table, + * in descending order of support. + * @param mixed $mapSupport the frequencies of each item (key: item value: support) + */ + function createHeaderList($mapSupport) + { + // create an array to store the header list with + // all the items stored in the map received as parameter + $this->headerList = array_keys($this->mapItemNodes); + + $sortFunction = function ($id1, $id2) use ($mapSupport) { + // compare the support + $compare = $mapSupport[$id2] - $mapSupport[$id1]; + // if the same frequency, we check the lexical ordering! + // otherwise we use the support + return ($compare == 0) ? ($id1 - $id2) : $compare; + }; + // sort the header table by decreasing order of support + usort($this->headerList, $sortFunction); + + } + + /** + * Method for getting a string representation of the CP-tree + * (to be used for debugging purposes). + * @return string + */ + public function __toString() + { + $temp = "F HeaderList: " . print_r($this->headerList, true) . "\n root: " . $this->root; + return $temp; + } + + +} diff --git a/fpgrowth/Itemset.php b/fpgrowth/Itemset.php new file mode 100644 index 0000000..949b167 --- /dev/null +++ b/fpgrowth/Itemset.php @@ -0,0 +1,148 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + + +/** + * This class represents an itemset (a set of items) implemented as an array of integers with + * a variable to store the support count of the itemset. + * + * @author Jesus Gazol + */ +class Itemset +{ + /** the array of items **/ + public $itemset = []; + + /** the support of this itemset */ + public $support = 0; + + public function __construct($itemset = false) + { + if ($itemset != false) { + $this->itemset = $itemset; + } + } + + /** + * Get the items as array + * @return mixed the items + */ + public function getItems() + { + return $this->itemset; + } + + /** + * Get the support of this itemset + */ + public function getAbsoluteSupport() + { + return $this->support; + } + + /** + * Get the size of this itemset + */ + public function size() + { + return count($this->itemset); + } + + /** + * Get the item at a given position in this itemset + * @param $position + * @return mixed + */ + public function get($position) + { + return $this->itemset[$position]; + } + + /** + * Set the support of this itemset + * @param $support int the support + */ + public function setAbsoluteSupport($support) + { + $this->support = $support; + } + + /** + * Increase the support of this itemset by 1 + */ + public function increaseTransactionCount() + { + $this->support++; + } + + /** + * This method return an itemset containing items that are included + * in this itemset and in a given itemset + * @param $itemset2 Itemset the given itemset + * @return Itemset the new itemset + */ + public function intersection($itemset2) + { + $resItemset = new Itemset(); + $resItemset->itemset = $this->intersectTwoSortedArrays($this->getItems(), $itemset2->getItems()); + return $resItemset; + } + + /** + * Intersection of two sorted arrays + * @param $array1 + * @param $array2 + * @return array + */ + public function intersectTwoSortedArrays($array1, $array2) + { + // create a new array having the smallest size between the two arrays + $newArraySize = (count($array1) < count($array2)) ? count($array1) : count($array2); + $newArray = new SplFixedArray($newArraySize); + + $pos1 = 0; + $pos2 = 0; + $posNewArray = 0; + while ($pos1 < count($array1) && $pos2 < count($array2)) { + if ($array1[$pos1] < $array2[$pos2]) { + $pos1++; + } else if ($array2[$pos2] < $array1[$pos1]) { + $pos2++; + } else { // if they are the same + $newArray[$posNewArray] = $array1[$pos1]; + $posNewArray++; + $pos1++; + $pos2++; + } + } + // return the subrange of the new array that is full. + return array_slice($newArray, 0, $posNewArray); + } + + +} diff --git a/fpgrowth/Itemsets.php b/fpgrowth/Itemsets.php new file mode 100644 index 0000000..29f6fb5 --- /dev/null +++ b/fpgrowth/Itemsets.php @@ -0,0 +1,125 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + + +/** + * This class represents a set of itemsets, where an itemset is an array of integers + * with an associated support count. Itemsets are ordered by size. For + * example, level 1 means itemsets of size 1 (that contains 1 item). + */ +class Itemsets +{ + /** We store the itemsets in a list named "levels". + * Position i in "levels" contains the list of itemsets of size i */ + private $levels = []; + /** the total number of itemsets **/ + private $itemsetsCount = 0; + /** a name that we give to these itemsets (e.g. "frequent itemsets") */ + private $name; + + /** + * Constructor + * @param string $name the name of these itemsets + */ + public function __construct($name) + { + $this->name = $name; + $this->levels[] = []; // We create an empty level 0 by + // default. + } + + /* + */ + public function printItemsets() + { + print(" ------- {$this->name} -------\n"); + $patternCount = 0; + $levelCount = 0; + // for each level (a level is a set of itemsets having the same number of items) + foreach ($this->levels as $level) { + // print how many items are contained in this level + print(" L {$levelCount} \n"); + // for each itemset + foreach ($level as $itemset) { + + // print the itemset + print(" pattern {$patternCount}: "); + $itemset->print(); + // print the support of this itemset + print("support : " . $itemset->getAbsoluteSupport() . "\n"); + $patternCount++; + } + $levelCount++; + } + print(" --------------------------------\n"); + } + + /* + * + */ + public function addItemset($itemset, $k) + { + while (count($this->levels) <= $k) { + $this->levels[] = []; + } + $this->levels[$k][] = $itemset; + $this->itemsetsCount++; + } + + /* + */ + public function getLevels() + { + return $this->levels; + + } + + /* + * + */ + public function getItemsetsCount() + { + return $this->itemsetsCount; + } + + /* + * + */ + public function setName($newName) + { + $this->name = $newName; + } + + /* + * + */ + public function decreaseItemsetCount() + { + $this->itemsetsCount--; + } +} diff --git a/fpgrowth/PrestashopWrapper.php b/fpgrowth/PrestashopWrapper.php new file mode 100644 index 0000000..f373c38 --- /dev/null +++ b/fpgrowth/PrestashopWrapper.php @@ -0,0 +1,182 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + +require_once "AlgoFPGrowth.php"; + +/** + * Class that wraps fpgrowth functionality for prestashop + * + * User: jesus gazol + * Date: 04/07/16 + * Time: 14:37 + */ +class PrestashopWrapper +{ + /** + * Number of transactions we are going to pull from the database + */ + const TRANSACTION_LIMIT = 10000; + const CART_LIMIT = 5000; + + /** + * Get the product counters from prestashop db + * @return array + */ + public function getProductCounters() + { + $sql = " + SELECT id_product, count(0) as counter + FROM ps_cart_product + GROUP BY id_product + ORDER BY id_cart DESC + LIMIT " . self::TRANSACTION_LIMIT; + $rows = \Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + $productDict = []; + foreach ($rows as $row) { + $productDict["{$row['id_product']}"] = $row['counter']; + } + return $productDict; + } + + /** + * Get the transactions from prestashop db + * @return mixed + */ + public function getTransactionsDb() + { + $sql = " + SELECT group_concat(id_product separator ' ') as transaction + FROM ps_cart_product + GROUP BY id_cart + ORDER BY id_cart DESC + LIMIT " . self::CART_LIMIT; + $rows = \Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + + $transactions = []; + foreach ($rows as $row) { + $transactions[] = explode(" ", $row['transaction']); + } + return $transactions; + } + + /** + * Run product association fpgrowth algorithm + * @param $minSupport float minimum support + * @return mixed Levels of association rules found + */ + public function getProductAssociationRules($minSupport = 0.02) + { + $algo = new AlgoFPGrowth(); + $patterns = $algo->runAlgorithm($this->getTransactionsDb(), $minSupport, $this->getProductCounters()); + return $patterns->getLevels(); + + } + + /** + * Save the product association rules, we are going to use only level 2 + * @param $levels mixed Association rules derived + */ + public function saveProductAssociationRules($levels) + { + if (count($levels[2]) > 0) { + #Truncate table + \Db::getInstance()->execute("TRUNCATE TABLE ps_crosseling_pair"); + #Add new rules + $sql = "INSERT INTO ps_crosseling_pair + (`id_product_main`, + `id_product_related`, + `support`) + VALUES "; + foreach ($levels[2] as $rule) { + $items = $rule->getItems(); + $sqlValues[] = "({$items[0]},{$items[1]}," . $rule->getAbsoluteSupport() . ")"; + } + $sql .= join(",", $sqlValues); + return \Db::getInstance()->execute($sql); + } + return false; + } + + /** + * Function to create the table needed in the database + */ + public static function createTable() + { + $sql = "CREATE TABLE " . _DB_PREFIX_ . "crossselling_pair ( + `id_crossselling_pair` INT NOT NULL AUTO_INCREMENT, + `id_product_main` INT NOT NULL, + `id_product_related` INT NOT NULL, + `support` INT NULL DEFAULT 0, + PRIMARY KEY (`id_crossselling_pair`), + INDEX `idx_pmain` (`id_product_main` ASC, `support` ASC)) + "; + \Db::getInstance()->execute($sql); + } + + /** + * Function that injects data into test database, do not use in production + */ + public static function injectDummyCarts() + { + $totalCarts = 100; + $totalProducts = 3; + $idProducts = range(1, 7); + $idCustomers = range(1, 10); + #Create cart + for ($i = 1; $i < $totalCarts; $i++) { + $cart = new \Cart(); + $cart->id_shop_group = 1; + $cart->id_shop = 1; + $cart->id_customer = array_rand($idCustomers); + $cart->id_carrier = 2; + $cart->id_address_delivery = 1; + $cart->id_address_invoice = 1; + $cart->id_currency = 1; + $cart->id_lang = 1; + $cart->secure_key = ""; + // Save new cart + $cart->add(); + #Insert products + for ($j = 0; $j <= $totalProducts; $j++) { + $sql = "INSERT INTO `prestashop`.`ps_cart_product` + (`id_cart`, + `id_product`, + `id_address_delivery`, + `id_shop`, + `id_product_attribute`, + `id_customization`, + `quantity` + ) + VALUES + (" . $cart->id . "," . $idProducts[array_rand($idProducts)] . ", 1, 1, 0, 0, 1 )"; + \Db::getInstance()->executeS($sql); + } + } + + } +} \ No newline at end of file diff --git a/fpgrowth/TransactionFileGenerator.php b/fpgrowth/TransactionFileGenerator.php new file mode 100644 index 0000000..330d602 --- /dev/null +++ b/fpgrowth/TransactionFileGenerator.php @@ -0,0 +1,45 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace fpgrowth; + + +class TransactionFileGenerator extends \SplFileObject +{ + + function current() + { + $line = trim(parent::current()); + if (strlen($line) > 0) { + return explode(" ", $line); + } else { + return []; + } + + } + + +} \ No newline at end of file diff --git a/fpgrowth/tests/AlgoFPGrowthTest.php b/fpgrowth/tests/AlgoFPGrowthTest.php new file mode 100644 index 0000000..a728af6 --- /dev/null +++ b/fpgrowth/tests/AlgoFPGrowthTest.php @@ -0,0 +1,110 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +require_once("../AlgoFPGrowth.php"); + +/** + * Generated by PHPUnit_SkeletonGenerator on 2016-07-01 at 12:42:07. + */ +class AlgoFPGrowthTest extends PHPUnit_Framework_TestCase +{ + /** + * @var fpgrowth\AlgoFPGrowth + */ + protected $object; + + private $filename; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() + { + $this->object = new fpgrowth\AlgoFPGrowth; + $this->filename = "sample.txt"; + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() + { + } + + /** + * @covers AlgoFPGrowth::runAlgorithm + * @todo Implement testRunAlgorithm(). + */ + public function testRunAlgorithm() + { + $transactionData = new \fpgrowth\TransactionFileGenerator($this->filename); + $minSupport = 0.2; + $patterns = $this->object->runAlgorithm($transactionData, $minSupport); + $levels = $patterns->getLevels(); + $this->assertEquals($levels[3][0]->itemset, ['1', '2', '3']); + + } + + public function testScanDatabaseToDetermineFrequencyOfSingleItems() + { + $transactionData = new \fpgrowth\TransactionFileGenerator($this->filename); + $mapSupport = $this->object->scanDatabaseToDetermineFrequencyOfSingleItems($transactionData); + $expected = [ + '3' => 4, + '4' => 3, + '1' => 4, + '2' => 4, + '5' => 1 + ]; + $this->assertEquals($mapSupport, $expected); + } + + public function testSortTransaction() + { + $testMap = [ + '3' => 4, + '4' => 3, + '1' => 4, + '2' => 4, + '5' => 1 + ]; + $transaction = ['5', '4', '1']; + $this->object->sortTransaction($transaction, $testMap); + $this->assertEquals($transaction, ['1', '4', '5']); + } + + public function testOnePathFile() + { + $transactionData = new \fpgrowth\TransactionFileGenerator("sample_onepath.txt"); + $patterns = $this->object->runAlgorithm($transactionData, 0.2); + $levels = $patterns->getLevels(); + $this->assertEquals($levels[3][0]->itemset, ['a', 'b', 'c']); + } + + +} diff --git a/fpgrowth/tests/sample.txt b/fpgrowth/tests/sample.txt new file mode 100644 index 0000000..cd5016f --- /dev/null +++ b/fpgrowth/tests/sample.txt @@ -0,0 +1,7 @@ +3 4 +3 4 +1 2 +1 2 3 4 +1 2 +1 2 3 +5 \ No newline at end of file diff --git a/fpgrowth/tests/sample_onepath.txt b/fpgrowth/tests/sample_onepath.txt new file mode 100644 index 0000000..b82c4b2 --- /dev/null +++ b/fpgrowth/tests/sample_onepath.txt @@ -0,0 +1,5 @@ +a b c +a b c +a b c +a b c +a b c From 417af0500479e644da898bf9c6bb1c90488ba93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Gazol?= Date: Thu, 14 Jul 2016 15:59:01 +0200 Subject: [PATCH 6/6] Fixed bug in support, added reverse mapping for products --- fpgrowth/AlgoFPGrowth.php | 21 +++++++++++++-------- fpgrowth/FPNode.php | 5 ++--- fpgrowth/PrestashopWrapper.php | 10 +++++++--- fpgrowth/tests/AlgoFPGrowthTest.php | 2 ++ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/fpgrowth/AlgoFPGrowth.php b/fpgrowth/AlgoFPGrowth.php index 637bdb4..99546a4 100644 --- a/fpgrowth/AlgoFPGrowth.php +++ b/fpgrowth/AlgoFPGrowth.php @@ -74,10 +74,12 @@ class AlgoFPGrowth * @param mixed $input an array like db of transactions. * @param float $minsupp the minimum support threshold. * @param mixed $mapSupport + * @param int $transactionCount number of transactions passed * @return fpgrowth /Itemsets result if no output file path is provided. */ - public function runAlgorithm($input, $minsupp, $mapSupport = null) + public function runAlgorithm($input, $minsupp, $mapSupport = null, $transactionCount = null) { + // record start time $this->startTimestamp = microtime(true); // number of itemsets found @@ -93,6 +95,9 @@ public function runAlgorithm($input, $minsupp, $mapSupport = null) if ($mapSupport == null) { $mapSupport = $this->scanDatabaseToDetermineFrequencyOfSingleItems($input); } + if($transactionCount != null){ + $this->transactionCount = $transactionCount; + } // convert the minimum support as percentage to a // relative minimum support @@ -109,7 +114,7 @@ public function runAlgorithm($input, $minsupp, $mapSupport = null) $transaction = []; // for each item in the transaction add items that have the minimum support foreach ($transactionLine as $item) { - if ($mapSupport[$item] >= $this->minSupportRelative) { + if ($mapSupport["$item"] >= $this->minSupportRelative) { $transaction[] = $item; } } @@ -122,6 +127,7 @@ public function runAlgorithm($input, $minsupp, $mapSupport = null) // We create the header table for the tree using the calculated support of single items $tree->createHeaderList($mapSupport); + // (5) We start to mine the FP-Tree by calling the recursive method. // Initially, the prefix alpha is empty. // if at least an item is frequent @@ -153,7 +159,7 @@ public function sortTransaction(&$transaction, $mapSupport) { $sortFunction = function ($id1, $id2) use (&$mapSupport) { // compare the support - $compare = $mapSupport[$id2] - $mapSupport[$id1]; + $compare = $mapSupport["$id2"] - $mapSupport["$id1"]; // if the same frequency, we check the lexical ordering! // otherwise we use the support return ($compare == 0) ? ($id1 - $id2) : $compare; @@ -216,7 +222,7 @@ private function fpgrowth($tree, $prefix, $prefixLength, $prefixSupport, $mapSup // get the item $item = $tree->headerList[$i]; // get the item support - $support = $mapSupport[$item]; + $support = $mapSupport["$item"]; // Create Beta by concatening prefix Alpha by adding the current item to alpha $prefix[$prefixLength] = $item; // calculate the support of the new prefix beta @@ -247,12 +253,12 @@ private function fpgrowth($tree, $prefix, $prefixLength, $prefixSupport, $mapSup $prefixPath[] = $parent; // FOR EACH PATTERN WE ALSO UPDATE THE ITEM SUPPORT AT THE SAME TIME // if the first time we see that node id - if (!isset($mapSupportBeta[$parent->itemID])) { + if (!isset($mapSupportBeta["{$parent->itemID}"])) { // just add the path count - $mapSupportBeta[$parent->itemID] = $pathCount; + $mapSupportBeta["{$parent->itemID}"] = $pathCount; } else { // otherwise, make the sum with the value already stored - $mapSupportBeta[$parent->itemID] = $mapSupportBeta[$parent->itemID] + $pathCount; + $mapSupportBeta["{$parent->itemID}"] = $mapSupportBeta["{$parent->itemID}"] + $pathCount; } $parent = $parent->parent; } @@ -273,7 +279,6 @@ private function fpgrowth($tree, $prefix, $prefixLength, $prefixSupport, $mapSup // Mine recursively the Beta tree if the root has child(s) if (count($treeBeta->root->childs) > 0) { - // Create the header list. $treeBeta->createHeaderList($mapSupportBeta); // recursive call diff --git a/fpgrowth/FPNode.php b/fpgrowth/FPNode.php index d677989..0e8d5fa 100644 --- a/fpgrowth/FPNode.php +++ b/fpgrowth/FPNode.php @@ -65,10 +65,9 @@ function getChildWithID($id) { * @return string a string */ public function __toString() { - $strOut = "( {$this->itemID} count={$this->counter})\n"; - $newindent = " "; + $strOut = "( id={$this->itemID} count={$this->counter})\n"; foreach($this->childs as $child) { - $strOut .= $newindent . $child; + $strOut .= " " . $child; } return $strOut; } diff --git a/fpgrowth/PrestashopWrapper.php b/fpgrowth/PrestashopWrapper.php index f373c38..57a3c61 100644 --- a/fpgrowth/PrestashopWrapper.php +++ b/fpgrowth/PrestashopWrapper.php @@ -92,7 +92,8 @@ public function getTransactionsDb() public function getProductAssociationRules($minSupport = 0.02) { $algo = new AlgoFPGrowth(); - $patterns = $algo->runAlgorithm($this->getTransactionsDb(), $minSupport, $this->getProductCounters()); + $transactions = $this->getTransactionsDb(); + $patterns = $algo->runAlgorithm($transactions, $minSupport, $this->getProductCounters(), count($transactions)); return $patterns->getLevels(); } @@ -105,9 +106,9 @@ public function saveProductAssociationRules($levels) { if (count($levels[2]) > 0) { #Truncate table - \Db::getInstance()->execute("TRUNCATE TABLE ps_crosseling_pair"); + \Db::getInstance()->execute("TRUNCATE TABLE ps_crossselling_pair"); #Add new rules - $sql = "INSERT INTO ps_crosseling_pair + $sql = "INSERT INTO ps_crossselling_pair (`id_product_main`, `id_product_related`, `support`) @@ -115,8 +116,11 @@ public function saveProductAssociationRules($levels) foreach ($levels[2] as $rule) { $items = $rule->getItems(); $sqlValues[] = "({$items[0]},{$items[1]}," . $rule->getAbsoluteSupport() . ")"; + #Add also reversed order + $sqlValues[] = "({$items[1]},{$items[0]}," . $rule->getAbsoluteSupport() . ")"; } $sql .= join(",", $sqlValues); + return \Db::getInstance()->execute($sql); } return false; diff --git a/fpgrowth/tests/AlgoFPGrowthTest.php b/fpgrowth/tests/AlgoFPGrowthTest.php index a728af6..b717278 100644 --- a/fpgrowth/tests/AlgoFPGrowthTest.php +++ b/fpgrowth/tests/AlgoFPGrowthTest.php @@ -100,10 +100,12 @@ public function testSortTransaction() public function testOnePathFile() { + $transactionData = new \fpgrowth\TransactionFileGenerator("sample_onepath.txt"); $patterns = $this->object->runAlgorithm($transactionData, 0.2); $levels = $patterns->getLevels(); $this->assertEquals($levels[3][0]->itemset, ['a', 'b', 'c']); + }