Source code for fraudar.graph

#
#  graph.py
#
#  Copyright (c) 2016-2023 Junpei Kawamoto
#
#  This file is part of rgmining-fraudar.
#
#  rgmining-fraudar is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  rgmining-fraudar is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with rgmining-fraudar. If not, see <http://www.gnu.org/licenses/>.
#
"""Provide a review graph which runs Fraudar algorithm.
"""
import tempfile
from bisect import bisect_left
from collections import defaultdict
from typing import Any, Final, Protocol

import numpy as np

from fraudar.export import greedy
from fraudar.export.greedy import logWeightedAveDegree


[docs]class Node: """Node of the ReviewGraph. A node has a name and a link to the graph. It also implements :meth:`__hash__` function so that each node can be stored in dictionaries. Args: graph: graph object this node belongs to. name: name of this node. """ graph: Final["ReviewGraph"] """The graph object this node belongs to.""" name: Final[str] """Name of this node.""" __slots__ = ("graph", "name") def __init__(self, graph: "ReviewGraph", name: str) -> None: """Construct a node instance. Args: graph: graph object this node belongs to. name: name of this node. """ self.graph = graph self.name = name def __hash__(self) -> int: """Returns a hash value of this instance.""" return 13 * hash(type(self)) + 17 * hash(self.name) def __lt__(self, other: "Node") -> bool: return self.name.__lt__(other.name)
[docs]class Reviewer(Node): """A node type representing a reviewer. Use :meth:`ReviewGraph.new_reviewer` to create a new reviewer object instead of using this constructor directory. Args: graph: graph object this reviewer belongs to. name: name of this reviewer. """ anomalous_score: float """anomalous score of this reviewer.""" __slots__ = ("anomalous_score",) def __init__(self, graph: "ReviewGraph", name: str, anomalous_score: float = 0) -> None: super().__init__(graph, name) self.anomalous_score = anomalous_score
[docs]class Product(Node): """A node type representing a product. Use :meth:`ReviewGraph.new_product` to create a new product object instead of using this constructor directory. Args: graph: graph object this product belongs to. name: name of this product. """ __slots__ = () @property def summary(self) -> float: """Summary of ratings given to this product.""" reviewers = self.graph.reviews[self].keys() ratings = [self.graph.reviews[self][r] for r in reviewers] weights = [1 - r.anomalous_score for r in reviewers] if sum(weights) == 0: return float(np.mean(ratings)) else: return float(np.average(ratings, weights=weights))
class _Writable(Protocol): def write(self, s: str, /) -> int: ...
[docs]class ReviewGraph: """ReviewGraph is a simple bipartite graph representing review relation. Args: blocks: how many blocks to be detected. (default: 1) algo: algorithm used in fraudar, chosen from :meth:`aveDegree <fraudar.export.greedy.aveDegree>`, :meth:`sqrtWeightedAveDegree <fraudar.export.greedy.sqrtWeightedAveDegree>`, and :meth:`logWeightedAveDegree <fraudar.export.greedy.logWeightedAveDegree>`. (default: logWeightedAveDegree) """ reviewers: Final[list[Reviewer]] """Collection of reviewers.""" products: Final[list[Product]] """Collection of products.""" reviews: Final[defaultdict[Product, dict[Reviewer, float]]] """Collection of reviews. reviews is a dictionary of which key is a product and value is another dictionary of which key is a reviewer and value is a rating from the reviewer to the product. """ _algo: Final[Any] _blocks: Final[int] def __init__(self, blocks: int = 1, algo: Any = logWeightedAveDegree) -> None: self.reviewers = [] self.products = [] self.reviews = defaultdict(dict) self._algo = algo self._blocks = blocks
[docs] def new_reviewer(self, name: str, **_kwargs: Any) -> Reviewer: """Create a new reviewer. Args: name: name of the new reviewer. Returns: a new reviewer object. """ r = Reviewer(self, name) self.reviewers.append(r) return r
[docs] def new_product(self, name: str) -> Product: """Create a new product. Args: name: name of the new product. Returns: a new product object. """ p = Product(self, name) self.products.append(p) return p
[docs] def add_review(self, reviewer: Reviewer, product: Product, rating: float, **_kwargs: Any) -> float: """Add a review from a reviewer to a product. Args: reviewer: reviewer who posts the review. product: product which receives the review. rating: the review score. Returns: added review score. """ self.reviews[product][reviewer] = rating return rating
[docs] def update(self) -> float: """Update anomalous scores by running a greedy algorithm. Returns: 0 """ with tempfile.NamedTemporaryFile(mode="w") as fp: # Store this graph to a temporal file. self._store_matrix(fp) fp.seek(0) # Run greedy algorithm. M = greedy.readData(fp.name) res = greedy.detectMultiple(M, self._algo, self._blocks) # Update anomalous scores. for block in res: for i in block[0][0]: self.reviewers[i].anomalous_score = 1 return 0
def _store_matrix(self, fp: _Writable) -> None: """Store this graph as a sparse matrix format. Args: fp: file-like object where the matrix to be written. """ self.reviewers.sort() self.products.sort() for p in self.reviews: j = bisect_left(self.products, p) for r in self.reviews[p]: i = bisect_left(self.reviewers, r) fp.write("{0} {1}\n".format(i, j))