Repository: firefly-cpp/NiaARM

Issue:
Zhang's metric
@zStupan, can Zhang's metric be added to the pool of implemented interestingness measures?

See example: https://medium.com/@iuyasik/market-basket-analysis-apriori-algorithm-using-zhangs-metric-708406fc5dfc

Discussion:

Patch:
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index d75a887..c48d740 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -334,6 +334,7 @@ The framework currently implements the following interest measures (metrics):
 - Comprehensibility
 - Netconf [#fn]_
 - Yule's Q [#fn]_
+- Zhang's Metric [#fn]_
 
 More information about these interest measures can be found in the API reference
 of the :class:`~niaarm.rule.Rule` class.
diff --git a/interest_measures.md b/interest_measures.md
index 3170684..a70183c 100644
--- a/interest_measures.md
+++ b/interest_measures.md
@@ -192,3 +192,19 @@ and 0 reflects independence)
 **Reference:** E. V. Altay and B. Alatas, "Sensitivity Analysis of MODENAR Method for Mining of Numeric Association
 Rules," 2019 1st International Informatics and Software Engineering Conference (UBMYK), 2019, pp. 1-6,
 doi: 10.1109/UBMYK48245.2019.8965539.
+
+# Zhang's Metric
+
+Zheng's metric measures the strength of association (positive or negative) between the antecedent and consequent, 
+taking into account both their co-occurrence and non-co-occurrence.
+
+```math
+zhang(X \implies Y) =
+\frac{conf(X \implies Y) - conf(\neg X \implies Y)}{max\{conf(X \implies Y), conf(\neg X \implies Y)\}}
+```
+
+**Range:** $`[-1, 1]`$ (-1 reflects total negative association, 1 reflects perfect positive association
+and 0 reflects independence)
+
+**Reference:** T. Zhang, “Association Rules,” in Knowledge Discovery and Data Mining. Current Issues and New 
+Applications, 2000, pp. 245–256. doi: 10.1007/3-540-45571-X_31. 
diff --git a/niaarm/rule.py b/niaarm/rule.py
index 37e4944..2d5e0d4 100644
--- a/niaarm/rule.py
+++ b/niaarm/rule.py
@@ -145,6 +145,16 @@ class Rule:
          **Reference:** E. V. Altay and B. Alatas, "Sensitivity Analysis of MODENAR Method for Mining of Numeric Association
          Rules," 2019 1st International Informatics and Software Engineering Conference (UBMYK), 2019, pp. 1-6,
          doi: 10.1109/UBMYK48245.2019.8965539.
+        zhang: Zheng's metric measures the strength of association (positive or negative) between the antecedent and consequent, taking into account both their co-occurrence and non-co-occurrence.
+
+         :math:`zhang(X \implies Y) =
+         \frac{conf(X \implies Y) - conf(\neg X \implies Y)}{max\{conf(X \implies Y), conf(\neg X \implies Y)\}}`
+
+         **Range:** :math:`[-1, 1]` (-1 reflects total negative association, 1 reflects perfect positive association
+         and 0 reflects independence)
+
+         **Reference:** T. Zhang, “Association Rules,” in Knowledge Discovery and Data Mining. Current Issues and New
+         Applications, 2000, pp. 245–256. doi: 10.1007/3-540-45571-X_31.
 
     """
 
@@ -176,6 +186,7 @@ class Rule:
         "comprehensibility",
         "netconf",
         "yulesq",
+        "zhang",
     )
 
     def __init__(self, antecedent, consequent, fitness=0.0, transactions=None):
@@ -304,6 +315,17 @@ class Rule:
             1 + len(self.antecedent) + len(self.consequent)
         )
 
+    @property
+    def zhang(self):
+        support_x = self.coverage
+        support_y = self.rhs_support
+        support = self.support
+
+        numerator = support - support_x * support_y
+        denominator = max(support * (1 - support_x), support_x * (support_y - support))
+
+        return numerator / denominator
+
     def __eq__(self, other):
         return (
             self.antecedent == other.antecedent and self.consequent == other.consequent
diff --git a/niaarm/rule_list.py b/niaarm/rule_list.py
index ae85dd8..614b639 100644
--- a/niaarm/rule_list.py
+++ b/niaarm/rule_list.py
@@ -122,6 +122,7 @@ class RuleList(UserList):
             f'Average comprehensibility: {self.mean("comprehensibility")}\n'
             f'Average netconf: {self.mean("netconf")}\n'
             f'Average Yule\'s Q: {self.mean("yulesq")}\n'
+            f'Average Zhang\'s Metric: {self.mean("zhang")}\n'
             f"Average antecedent length: {sum(len(rule.antecedent) for rule in self) / len(self)}\n"
             f"Average consequent length: {sum(len(rule.consequent) for rule in self) / len(self)}\n"
         )


Test patch:
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index f109e03..c0e8cb9 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -66,3 +66,7 @@ class TestMetrics(TestCase):
     def test_yulesq(self):
         self.assertAlmostEqual(self.rule_one.yulesq, (6 - 1) / (6 + 1))
         self.assertAlmostEqual(self.rule_two.yulesq, (6 - 1) / (6 + 1))
+
+    def test_zhang(self):
+        self.assertAlmostEqual(self.rule_one.zhang, 5 / 9)
+        self.assertAlmostEqual(self.rule_two.zhang, 5 / 8)