Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions mlxtend/frequent_patterns/association_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"jaccard",
"certainty",
"kulczynski",
"mutual_information",
]


Expand Down Expand Up @@ -142,6 +143,19 @@ def kulczynski_helper(sAC, sA, sC, disAC, disA, disC, dis_int, dis_int_):
kulczynski = (conf_AC + conf_CA) / 2
return kulczynski

def mutual_information_metric_helper(
sAC, sA, sC, disAC, disA, disC, dis_int, dis_int_
):
with np.errstate(divide="ignore", invalid="ignore"):
numerator = sAC
denominator = sA * sC
mi = np.where(
(numerator > 0) & (denominator > 0),
np.log2(numerator / denominator),
-np.inf,
)
return mi

def conviction_helper(conf, sC):
conviction = np.empty(conf.shape, dtype=float)
if not len(conviction.shape):
Expand Down Expand Up @@ -223,6 +237,9 @@ def certainty_metric_helper(sAC, sA, sC, disAC, disA, disC, dis_int, dis_int_):
"kulczynski": lambda sAC, sA, sC, _, __, ____, _____, ______: kulczynski_helper(
sAC, sA, sC, disAC, disA, disC, dis_int, dis_int_
),
"mutual_information": lambda sAC, sA, sC, _, __, ____, _____, ______: mutual_information_metric_helper(
sAC, sA, sC, disAC, disA, disC, dis_int, dis_int_
),
}

# check for metric compliance
Expand Down
80 changes: 70 additions & 10 deletions mlxtend/frequent_patterns/tests/test_association_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"jaccard",
"certainty",
"kulczynski",
"mutual_information",
]


Expand All @@ -65,15 +66,15 @@ def test_default():

expect = pd.DataFrame(
[
[(8,), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8],
[(6,), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8],
[(8, 3), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8],
[(8, 5), (3,), 0.6, 0.8, 0.6, 1.0, 1.25, 1.0, 0.12, np.inf, 0.5, 0.75, 1.0, 0.875],
[(8,), (3, 5), 0.6, 0.8, 0.6, 1.0, 1.25, 1.0, 0.12, np.inf, 0.5, 0.75, 1.0, 0.875],
[(3,), (5,), 0.8, 1.0, 0.8, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.8, 0.0, 0.9],
[(5,), (3,), 1.0, 0.8, 0.8, 0.8, 1.0, 1.0, 0.0, 1.0, 0.0, 0.8, 0.0, 0.9],
[(10,), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8],
[(8,), (3,), 0.6, 0.8, 0.6, 1.0, 1.25, 1.0, 0.12, np.inf, 0.5, 0.75, 1.0, 0.875],
[(8,), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8, 0.0],
[(6,), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8, 0.0],
[(8, 3), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8, 0.0],
[(8, 5), (3,), 0.6, 0.8, 0.6, 1.0, 1.25, 1.0, 0.12, np.inf, 0.5, 0.75, 1.0, 0.875, 0.0],
[(8,), (3, 5), 0.6, 0.8, 0.6, 1.0, 1.25, 1.0, 0.12, np.inf, 0.5, 0.75, 1.0, 0.875, 0.0],
[(3,), (5,), 0.8, 1.0, 0.8, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.8, 0.0, 0.9, 0.0],
[(5,), (3,), 1.0, 0.8, 0.8, 0.8, 1.0, 1.0, 0.0, 1.0, 0.0, 0.8, 0.0, 0.9, 0.0],
[(10,), (5,), 0.6, 1.0, 0.6, 1.0, 1.0, 1.0, 0.0, np.inf, 0, 0.6, 0.0, 0.8, 0.0],
[(8,), (3,), 0.6, 0.8, 0.6, 1.0, 1.25, 1.0, 0.12, np.inf, 0.5, 0.75, 1.0, 0.875, 0.0],
],

columns=columns_ordered,
Expand Down Expand Up @@ -120,6 +121,7 @@ def test_nullability():
0.667,
0,
0.833,
0.0,
],
[
(10, 5),
Expand All @@ -136,6 +138,7 @@ def test_nullability():
0.667,
0.0,
0.833,
0.0,
],
[
(10,),
Expand All @@ -152,6 +155,7 @@ def test_nullability():
0.615,
0.0,
0.833,
0.415,
],
[
(10,),
Expand All @@ -168,6 +172,7 @@ def test_nullability():
0.615,
0.0,
0.833,
0.415,
],
[
(10,),
Expand All @@ -184,6 +189,7 @@ def test_nullability():
0.615,
0,
0.833,
-0.169,
],
[
(3, 5),
Expand All @@ -200,6 +206,7 @@ def test_nullability():
0.615,
-0.333,
0.833,
-0.169,
],
[
(3,),
Expand All @@ -216,6 +223,7 @@ def test_nullability():
0.667,
0.0,
0.833,
0.0,
],
[
(3,),
Expand All @@ -232,6 +240,7 @@ def test_nullability():
0.615,
-0.333,
0.833,
-0.169,
],
[(3,), (5,), 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.0, np.inf, 0, 1.0, 0, 1.0],
[
Expand All @@ -249,6 +258,7 @@ def test_nullability():
0.667,
0,
0.833,
0.0,
],
[
(5,),
Expand All @@ -265,8 +275,25 @@ def test_nullability():
0.615,
-0.333,
0.833,
-0.169,
],
[
(5,),
(3,),
1.0,
1.0,
1.0,
1.0,
1.0,
0.8,
0.0,
np.inf,
0,
1.0,
0.0,
1.0,
0.0,
],
[(5,), (3,), 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.0, np.inf, 0, 1.0, 0.0, 1.0],
],
columns=columns_ordered,
)
Expand Down Expand Up @@ -335,6 +362,7 @@ def test_empty_result():
"jaccard",
"certainty",
"kulczynski",
"mutual_information",
]
)
res_df = association_rules(df_freq_items, len(df), min_threshold=2)
Expand Down Expand Up @@ -540,3 +568,35 @@ def test_with_empty_dataframe():
df_freq = df_freq_items_with_colnames.iloc[:0]
with pytest.raises(ValueError):
association_rules(df_freq, len(df))


def test_mutual_information_metric():
"""Test mutual_information metric returns correct values."""
import math

res_df = association_rules(
df_freq_items_with_colnames,
len(df),
metric="mutual_information",
min_threshold=-100,
)
assert "mutual_information" in res_df.columns

# Eggs -> Kidney Beans: sAC=0.6, sA=0.8, sC=1.0
# MI = log2(0.6 / (0.8 * 1.0)) = log2(0.75) approx -0.415
rule = res_df[
res_df["antecedents"].apply(lambda x: x == frozenset({"Eggs"}))
& res_df["consequents"].apply(lambda x: x == frozenset({"Kidney Beans"}))
]
assert len(rule) == 1
expected_mi = math.log2(0.6 / (0.8 * 1.0))
assert abs(rule["mutual_information"].values[0] - expected_mi) < 1e-6

# Milk -> Kidney Beans: sAC=0.6, sA=0.6, sC=1.0
# MI = log2(0.6 / (0.6 * 1.0)) = log2(1) = 0
milk_rule = res_df[
res_df["antecedents"].apply(lambda x: x == frozenset({"Milk"}))
& res_df["consequents"].apply(lambda x: x == frozenset({"Kidney Beans"}))
]
assert len(milk_rule) == 1
assert abs(milk_rule["mutual_information"].values[0]) < 1e-6
Loading