diff --git a/setup.py b/setup.py index b934283..e23d073 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="tc-core-analyzer-lib", - version="1.1.0", + version="1.2.0", author="Mohammad Amin Dadgar, TogetherCrew", maintainer="Mohammad Amin Dadgar", maintainer_email="dadgaramin96@gmail.com", diff --git a/tc_core_analyzer_lib/assess_engagement.py b/tc_core_analyzer_lib/assess_engagement.py index 58a5752..07e1ccd 100644 --- a/tc_core_analyzer_lib/assess_engagement.py +++ b/tc_core_analyzer_lib/assess_engagement.py @@ -15,6 +15,7 @@ assess_remainder, assess_still_active, assess_vital, + assess_inconsistent, ) from .utils.compute_interaction_per_acc import thr_int @@ -74,6 +75,11 @@ def compute( all_lurker, all_about_to_disengage, all_disengaged_in_past, + all_inconsistent, + all_new_consistent, + all_new_vital, + all_became_not_consistent, + all_became_unvital, ): """ Assess engagment levels for all active members in a time period @@ -244,6 +250,11 @@ def compute( all_paused[str(w_i)] - all_consistent[str(w_i)] ) + # # # INCONSISTENT + all_inconsistent[str(w_i)] = assess_inconsistent( + all_active, all_paused, all_new_active, all_consistent, w_i + ) + # # # SUBDIVIDE DISENGAGED TYPES # # # # make temporary dictionary for remaining disengaged members @@ -290,6 +301,26 @@ def compute( all_disengaged_were_consistently_active[str(w_i)] = set() all_disengaged_were_newly_active[str(w_i)] = set() + # # # DETECT CHANGES SINCE LAST PERIOD # # # + if w_i - WINDOW_D >= 0: + all_new_consistent[str(w_i)] = ( + all_consistent[str(w_i)] - all_consistent[str(w_i - WINDOW_D)] + ) + all_new_vital[str(w_i)] = ( + all_vital[str(w_i)] - all_vital[str(w_i - WINDOW_D)] + ) + all_became_not_consistent[str(w_i)] = ( + all_consistent[str(w_i - WINDOW_D)] - all_consistent[str(w_i)] + ) + all_became_unvital[str(w_i)] = ( + all_vital[str(w_i - WINDOW_D)] - all_vital[str(w_i)] + ) + else: + all_new_consistent[str(w_i)] = set() + all_new_vital[str(w_i)] = set() + all_became_not_consistent[str(w_i)] = set() + all_became_unvital[str(w_i)] = set() + return ( graph, all_joined, @@ -312,4 +343,9 @@ def compute( all_lurker, all_about_to_disengage, all_disengaged_in_past, + all_inconsistent, + all_new_consistent, + all_new_vital, + all_became_not_consistent, + all_became_unvital, ) diff --git a/tc_core_analyzer_lib/tests/integration/test_active_members.py b/tc_core_analyzer_lib/tests/integration/test_active_members.py index 96843ce..16d360d 100644 --- a/tc_core_analyzer_lib/tests/integration/test_active_members.py +++ b/tc_core_analyzer_lib/tests/integration/test_active_members.py @@ -38,6 +38,11 @@ def test_no_active(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } WINDOW_D = 7 @@ -142,6 +147,11 @@ def test_single_active(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } # time window WINDOW_D = 7 diff --git a/tc_core_analyzer_lib/tests/integration/test_all_active_fourteen_period.py b/tc_core_analyzer_lib/tests/integration/test_all_active_fourteen_period.py index 126faa5..fbda404 100644 --- a/tc_core_analyzer_lib/tests/integration/test_all_active_fourteen_period.py +++ b/tc_core_analyzer_lib/tests/integration/test_all_active_fourteen_period.py @@ -56,6 +56,11 @@ def test_all_active_fourteen_period(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } WINDOW_D = 7 diff --git a/tc_core_analyzer_lib/tests/integration/test_all_became_not_consistent.py b/tc_core_analyzer_lib/tests/integration/test_all_became_not_consistent.py new file mode 100644 index 0000000..12caad3 --- /dev/null +++ b/tc_core_analyzer_lib/tests/integration/test_all_became_not_consistent.py @@ -0,0 +1,149 @@ +# test all_active members using the interaction matrix +import numpy as np +from tc_core_analyzer_lib.assess_engagement import EngagementAssessment +from tc_core_analyzer_lib.utils.activity import DiscordActivity + + +def test_became_not_consistent(): + acc_names = [] + acc_count = 10 + + act_param = { + "INT_THR": 1, + "UW_DEG_THR": 1, + "PAUSED_T_THR": 1, + "CON_T_THR": 4, + "CON_O_THR": 3, + "EDGE_STR_THR": 5, + "UW_THR_DEG_THR": 5, + "VITAL_T_THR": 4, + "VITAL_O_THR": 3, + "STILL_T_THR": 2, + "STILL_O_THR": 2, + "DROP_H_THR": 2, + "DROP_I_THR": 1, + } + + for i in range(acc_count): + acc_names.append(f"user{i}") + + acc_names = np.array(acc_names) + + # four weeks + max_interval = 35 + + # preparing empty joined members dict + all_joined = dict( + zip(np.array(range(max_interval), dtype=str), np.repeat(set(), max_interval)) + ) + + activity_dict = { + "all_joined": {}, + "all_joined_day": all_joined, + "all_consistent": {}, + "all_vital": {}, + "all_active": {}, + "all_connected": {}, + "all_paused": {}, + "all_new_disengaged": {}, + "all_disengaged": {}, + "all_unpaused": {}, + "all_returned": {}, + "all_new_active": {}, + "all_still_active": {}, + "all_dropped": {}, + "all_disengaged_were_newly_active": {}, + "all_disengaged_were_consistently_active": {}, + "all_disengaged_were_vital": {}, + "all_lurker": {}, + "all_about_to_disengage": {}, + "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, + } + memberactivites = activity_dict.keys() + + int_mat = { + DiscordActivity.Reply: np.zeros((acc_count, acc_count)), + DiscordActivity.Mention: np.zeros((acc_count, acc_count)), + DiscordActivity.Reaction: np.zeros((acc_count, acc_count)), + } + int_mat[DiscordActivity.Reaction][0, 1] = 6 + + activities = [ + DiscordActivity.Reaction, + DiscordActivity.Mention, + DiscordActivity.Reply, + ] + + engagement = EngagementAssessment( + activities=activities, activities_ignore_0_axis=[], activities_ignore_1_axis=[] + ) + + # the analytics + for w_i in range(max_interval): + # time window + WINDOW_D = 7 + + (_, *activity_dict) = engagement.compute( + int_mat=int_mat, + w_i=w_i, + acc_names=acc_names, + act_param=act_param, + WINDOW_D=WINDOW_D, + **activity_dict, + ) + + if w_i == 14: + int_mat[DiscordActivity.Reaction][0, 1] = 0 + int_mat[DiscordActivity.Reaction][0, 2] = 0 + int_mat[DiscordActivity.Reaction][0, 3] = 0 + int_mat[DiscordActivity.Reaction][0, 4] = 0 + int_mat[DiscordActivity.Reaction][0, 5] = 0 + int_mat[DiscordActivity.Reaction][0, 6] = 0 + + activity_dict = dict(zip(memberactivites, activity_dict)) + + print("all_consistent:", activity_dict["all_consistent"]) + print("all_became_not_consistent:", activity_dict["all_became_not_consistent"]) + + assert activity_dict["all_became_not_consistent"] == { + "0": set(), + "1": set(), + "2": set(), + "3": set(), + "4": set(), + "5": set(), + "6": set(), + "7": set(), + "8": set(), + "9": set(), + "10": set(), + "11": set(), + "12": set(), + "13": set(), + "14": set(), + "15": set(), + "16": set(), + "17": set(), + "18": set(), + "19": set(), + "20": set(), + "21": set(), + "22": set(), + "23": set(), + "24": set(), + "25": set(), + "26": set(), + "27": set(), + "28": {"user0", "user1"}, + "29": set(), + "30": set(), + "31": set(), + "32": set(), + "33": set(), + "34": set(), + } diff --git a/tc_core_analyzer_lib/tests/integration/test_all_became_unvital.py b/tc_core_analyzer_lib/tests/integration/test_all_became_unvital.py new file mode 100644 index 0000000..1c355c2 --- /dev/null +++ b/tc_core_analyzer_lib/tests/integration/test_all_became_unvital.py @@ -0,0 +1,155 @@ +# test all_active members using the interaction matrix +import numpy as np +from tc_core_analyzer_lib.assess_engagement import EngagementAssessment +from tc_core_analyzer_lib.utils.activity import DiscordActivity + + +def test_all_became_unvital(): + acc_names = [] + acc_count = 10 + + act_param = { + "INT_THR": 1, + "UW_DEG_THR": 1, + "PAUSED_T_THR": 1, + "CON_T_THR": 4, + "CON_O_THR": 3, + "EDGE_STR_THR": 5, + "UW_THR_DEG_THR": 5, + "VITAL_T_THR": 4, + "VITAL_O_THR": 3, + "STILL_T_THR": 2, + "STILL_O_THR": 2, + "DROP_H_THR": 2, + "DROP_I_THR": 1, + } + + for i in range(acc_count): + acc_names.append(f"user{i}") + + acc_names = np.array(acc_names) + + # four weeks + max_interval = 35 + + # preparing empty joined members dict + all_joined = dict( + zip(np.array(range(max_interval), dtype=str), np.repeat(set(), max_interval)) + ) + + activity_dict = { + "all_joined": {}, + "all_joined_day": all_joined, + "all_consistent": {}, + "all_vital": {}, + "all_active": {}, + "all_connected": {}, + "all_paused": {}, + "all_new_disengaged": {}, + "all_disengaged": {}, + "all_unpaused": {}, + "all_returned": {}, + "all_new_active": {}, + "all_still_active": {}, + "all_dropped": {}, + "all_disengaged_were_newly_active": {}, + "all_disengaged_were_consistently_active": {}, + "all_disengaged_were_vital": {}, + "all_lurker": {}, + "all_about_to_disengage": {}, + "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, + } + memberactivites = activity_dict.keys() + + int_mat = { + DiscordActivity.Reply: np.zeros((acc_count, acc_count)), + DiscordActivity.Mention: np.zeros((acc_count, acc_count)), + DiscordActivity.Reaction: np.zeros((acc_count, acc_count)), + } + # `user_0` intracting with `user_1`, `user_2`, `user_3`, `user_4`, `user_5` + int_mat[DiscordActivity.Reaction][0, 1] = 6 + int_mat[DiscordActivity.Reaction][0, 2] = 6 + int_mat[DiscordActivity.Reaction][0, 3] = 6 + int_mat[DiscordActivity.Reaction][0, 4] = 6 + int_mat[DiscordActivity.Reaction][0, 5] = 6 + int_mat[DiscordActivity.Reaction][0, 6] = 6 + + activities = [ + DiscordActivity.Reaction, + DiscordActivity.Mention, + DiscordActivity.Reply, + ] + + engagement = EngagementAssessment( + activities=activities, activities_ignore_0_axis=[], activities_ignore_1_axis=[] + ) + + # the analytics + for w_i in range(max_interval): + # time window + WINDOW_D = 7 + + (_, *activity_dict) = engagement.compute( + int_mat=int_mat, + w_i=w_i, + acc_names=acc_names, + act_param=act_param, + WINDOW_D=WINDOW_D, + **activity_dict, + ) + + if w_i == 14: + int_mat[DiscordActivity.Reaction][0, 1] = 0 + int_mat[DiscordActivity.Reaction][0, 2] = 0 + int_mat[DiscordActivity.Reaction][0, 3] = 0 + int_mat[DiscordActivity.Reaction][0, 4] = 0 + int_mat[DiscordActivity.Reaction][0, 5] = 0 + int_mat[DiscordActivity.Reaction][0, 6] = 0 + + activity_dict = dict(zip(memberactivites, activity_dict)) + + print("all_consistent:", activity_dict["all_consistent"]) + print("all_became_not_consistent:", activity_dict["all_became_not_consistent"]) + + assert activity_dict["all_became_not_consistent"] == { + "0": set(), + "1": set(), + "2": set(), + "3": set(), + "4": set(), + "5": set(), + "6": set(), + "7": set(), + "8": set(), + "9": set(), + "10": set(), + "11": set(), + "12": set(), + "13": set(), + "14": set(), + "15": set(), + "16": set(), + "17": set(), + "18": set(), + "19": set(), + "20": set(), + "21": set(), + "22": set(), + "23": set(), + "24": set(), + "25": set(), + "26": set(), + "27": set(), + "28": {"user0", "user1", "user2", "user3", "user4", "user5", "user6"}, + "29": set(), + "30": set(), + "31": set(), + "32": set(), + "33": set(), + "34": set(), + } diff --git a/tc_core_analyzer_lib/tests/integration/test_all_new_consistent.py b/tc_core_analyzer_lib/tests/integration/test_all_new_consistent.py new file mode 100644 index 0000000..8e49ece --- /dev/null +++ b/tc_core_analyzer_lib/tests/integration/test_all_new_consistent.py @@ -0,0 +1,140 @@ +import numpy as np +from tc_core_analyzer_lib.assess_engagement import EngagementAssessment +from tc_core_analyzer_lib.utils.activity import DiscordActivity + + +def test_all_new_consistent(): + """ + test all_new_consistent members category + """ + acc_names = [] + acc_count = 5 + for i in range(5): + acc_names.append(f"user{i}") + + acc_names = np.array(acc_names) + + # four weeks + max_interval = 28 + + # preparing empty joined members dict + all_joined = dict( + zip(np.array(range(max_interval), dtype=str), np.repeat(set(), max_interval)) + ) + + activity_dict = { + "all_joined": {}, + "all_joined_day": all_joined, + "all_consistent": {}, + "all_vital": {}, + "all_active": {}, + "all_connected": {}, + "all_paused": {}, + "all_new_disengaged": {}, + "all_disengaged": {}, + "all_unpaused": {}, + "all_returned": {}, + "all_new_active": {}, + "all_still_active": {}, + "all_dropped": {}, + "all_disengaged_were_newly_active": {}, + "all_disengaged_were_consistently_active": {}, + "all_disengaged_were_vital": {}, + "all_lurker": {}, + "all_about_to_disengage": {}, + "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, + } + memberactivities = activity_dict.keys() + + act_param = { + "INT_THR": 1, + "UW_DEG_THR": 1, + "PAUSED_T_THR": 1, + "CON_T_THR": 4, + "CON_O_THR": 3, + "EDGE_STR_THR": 5, + "UW_THR_DEG_THR": 5, + "VITAL_T_THR": 4, + "VITAL_O_THR": 3, + "STILL_T_THR": 2, + "STILL_O_THR": 2, + "DROP_H_THR": 2, + "DROP_I_THR": 1, + } + + int_mat = { + DiscordActivity.Reply: np.zeros((acc_count, acc_count)), + DiscordActivity.Mention: np.zeros((acc_count, acc_count)), + DiscordActivity.Reaction: np.zeros((acc_count, acc_count)), + } + + # `user_1` intracting with `user_2` + int_mat[DiscordActivity.Reaction][0, 1] = 2 + + activities = [ + DiscordActivity.Reaction, + DiscordActivity.Mention, + DiscordActivity.Reply, + ] + + engagement = EngagementAssessment( + activities=activities, activities_ignore_0_axis=[], activities_ignore_1_axis=[] + ) + + # the analytics + for w_i in range(max_interval): + # time window + WINDOW_D = 7 + + (_, *activity_dict) = engagement.compute( + int_mat=int_mat, + w_i=w_i, + acc_names=acc_names, + act_param=act_param, + WINDOW_D=WINDOW_D, + **activity_dict, + ) + if w_i == 14: + int_mat[DiscordActivity.Reaction][0, 1] = 0 + + activity_dict = dict(zip(memberactivities, activity_dict)) + + print("all_consistent:", activity_dict["all_consistent"]) + print("all_paused:", activity_dict["all_paused"]) + print("all_new_consistent:", activity_dict["all_new_consistent"]) + + assert activity_dict["all_new_consistent"] == { + "0": set(), + "1": set(), + "2": set(), + "3": set(), + "4": set(), + "5": set(), + "6": set(), + "7": set(), + "8": set(), + "9": set(), + "10": set(), + "11": set(), + "12": set(), + "13": set(), + "14": {"user0", "user1"}, + "15": set(), + "16": set(), + "17": set(), + "18": set(), + "19": set(), + "20": set(), + "21": set(), + "22": set(), + "23": set(), + "24": set(), + "25": set(), + "26": set(), + "27": set(), + } diff --git a/tc_core_analyzer_lib/tests/integration/test_all_new_vital.py b/tc_core_analyzer_lib/tests/integration/test_all_new_vital.py new file mode 100644 index 0000000..fccdf4e --- /dev/null +++ b/tc_core_analyzer_lib/tests/integration/test_all_new_vital.py @@ -0,0 +1,148 @@ +# test all_active members using the interaction matrix +import numpy as np +from tc_core_analyzer_lib.assess_engagement import EngagementAssessment +from tc_core_analyzer_lib.utils.activity import DiscordActivity + + +def test_new_vital(): + acc_names = [] + acc_count = 10 + + act_param = { + "INT_THR": 1, + "UW_DEG_THR": 1, + "PAUSED_T_THR": 1, + "CON_T_THR": 4, + "CON_O_THR": 3, + "EDGE_STR_THR": 5, + "UW_THR_DEG_THR": 5, + "VITAL_T_THR": 4, + "VITAL_O_THR": 3, + "STILL_T_THR": 2, + "STILL_O_THR": 2, + "DROP_H_THR": 2, + "DROP_I_THR": 1, + } + + for i in range(acc_count): + acc_names.append(f"user{i}") + + acc_names = np.array(acc_names) + + # four weeks + max_interval = 35 + + # preparing empty joined members dict + all_joined = dict( + zip(np.array(range(max_interval), dtype=str), np.repeat(set(), max_interval)) + ) + + activity_dict = { + "all_joined": {}, + "all_joined_day": all_joined, + "all_consistent": {}, + "all_vital": {}, + "all_active": {}, + "all_connected": {}, + "all_paused": {}, + "all_new_disengaged": {}, + "all_disengaged": {}, + "all_unpaused": {}, + "all_returned": {}, + "all_new_active": {}, + "all_still_active": {}, + "all_dropped": {}, + "all_disengaged_were_newly_active": {}, + "all_disengaged_were_consistently_active": {}, + "all_disengaged_were_vital": {}, + "all_lurker": {}, + "all_about_to_disengage": {}, + "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, + } + memberactivites = activity_dict.keys() + + int_mat = { + DiscordActivity.Reply: np.zeros((acc_count, acc_count)), + DiscordActivity.Mention: np.zeros((acc_count, acc_count)), + DiscordActivity.Reaction: np.zeros((acc_count, acc_count)), + } + # `user_0` intracting with `user_1`, `user_2`, `user_3`, `user_4`, `user_5` + # at least 5 times was needed + int_mat[DiscordActivity.Reaction][0, 1] = 6 + int_mat[DiscordActivity.Reaction][0, 2] = 6 + int_mat[DiscordActivity.Reaction][0, 3] = 6 + int_mat[DiscordActivity.Reaction][0, 4] = 6 + int_mat[DiscordActivity.Reaction][0, 5] = 6 + int_mat[DiscordActivity.Reaction][0, 6] = 6 + + activities = [ + DiscordActivity.Reaction, + DiscordActivity.Mention, + DiscordActivity.Reply, + ] + + engagement = EngagementAssessment( + activities=activities, activities_ignore_0_axis=[], activities_ignore_1_axis=[] + ) + + # the analytics + for w_i in range(max_interval): + # time window + WINDOW_D = 7 + + (_, *activity_dict) = engagement.compute( + int_mat=int_mat, + w_i=w_i, + acc_names=acc_names, + act_param=act_param, + WINDOW_D=WINDOW_D, + **activity_dict, + ) + + activity_dict = dict(zip(memberactivites, activity_dict)) + + print("all_vital:", activity_dict["all_vital"]) + print("all_new_vital:", activity_dict["all_new_vital"]) + + assert activity_dict["all_new_vital"] == { + "0": set(), + "1": set(), + "2": set(), + "3": set(), + "4": set(), + "5": set(), + "6": set(), + "7": set(), + "8": set(), + "9": set(), + "10": set(), + "11": set(), + "12": set(), + "13": set(), + "14": set(), + "15": set(), + "16": set(), + "17": set(), + "18": set(), + "19": set(), + "20": set(), + "21": {"user0"}, + "22": {"user0"}, + "23": {"user0"}, + "24": {"user0"}, + "25": {"user0"}, + "26": {"user0"}, + "27": {"user0"}, + "28": set(), # user0 is vital but new_vital + "29": set(), + "30": set(), + "31": set(), + "32": set(), + "33": set(), + "34": set(), + } diff --git a/tc_core_analyzer_lib/tests/integration/test_consistently_active.py b/tc_core_analyzer_lib/tests/integration/test_consistently_active.py index 1129b28..48fd02e 100644 --- a/tc_core_analyzer_lib/tests/integration/test_consistently_active.py +++ b/tc_core_analyzer_lib/tests/integration/test_consistently_active.py @@ -43,6 +43,11 @@ def test_two_consistently_active(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_disengaged_members.py b/tc_core_analyzer_lib/tests/integration/test_disengaged_members.py index ba1b7a2..0767fe1 100644 --- a/tc_core_analyzer_lib/tests/integration/test_disengaged_members.py +++ b/tc_core_analyzer_lib/tests/integration/test_disengaged_members.py @@ -40,6 +40,11 @@ def test_disengaged_members(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_disengaged_were_consistently_active.py b/tc_core_analyzer_lib/tests/integration/test_disengaged_were_consistently_active.py index 9fd21da..94fa34a 100644 --- a/tc_core_analyzer_lib/tests/integration/test_disengaged_were_consistently_active.py +++ b/tc_core_analyzer_lib/tests/integration/test_disengaged_were_consistently_active.py @@ -40,6 +40,11 @@ def test_disengaged_were_consistent(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_disengaged_were_newly_active.py b/tc_core_analyzer_lib/tests/integration/test_disengaged_were_newly_active.py index 526215c..9ee1df9 100644 --- a/tc_core_analyzer_lib/tests/integration/test_disengaged_were_newly_active.py +++ b/tc_core_analyzer_lib/tests/integration/test_disengaged_were_newly_active.py @@ -57,6 +57,11 @@ def test_disengaged_newly_active(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_disengaged_were_vital.py b/tc_core_analyzer_lib/tests/integration/test_disengaged_were_vital.py index 93e6c93..870fac5 100644 --- a/tc_core_analyzer_lib/tests/integration/test_disengaged_were_vital.py +++ b/tc_core_analyzer_lib/tests/integration/test_disengaged_were_vital.py @@ -57,6 +57,11 @@ def test_disengaged_were_vital(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_inconsistently_active.py b/tc_core_analyzer_lib/tests/integration/test_inconsistently_active.py new file mode 100644 index 0000000..53ee525 --- /dev/null +++ b/tc_core_analyzer_lib/tests/integration/test_inconsistently_active.py @@ -0,0 +1,140 @@ +import numpy as np +from tc_core_analyzer_lib.assess_engagement import EngagementAssessment +from tc_core_analyzer_lib.utils.activity import DiscordActivity + + +def test_inconsistently_active(): + """ + test conssitently_active members category + """ + acc_names = [] + acc_count = 5 + for i in range(5): + acc_names.append(f"user{i}") + + acc_names = np.array(acc_names) + + # four weeks + max_interval = 28 + + # preparing empty joined members dict + all_joined = dict( + zip(np.array(range(max_interval), dtype=str), np.repeat(set(), max_interval)) + ) + + activity_dict = { + "all_joined": {}, + "all_joined_day": all_joined, + "all_consistent": {}, + "all_vital": {}, + "all_active": {}, + "all_connected": {}, + "all_paused": {}, + "all_new_disengaged": {}, + "all_disengaged": {}, + "all_unpaused": {}, + "all_returned": {}, + "all_new_active": {}, + "all_still_active": {}, + "all_dropped": {}, + "all_disengaged_were_newly_active": {}, + "all_disengaged_were_consistently_active": {}, + "all_disengaged_were_vital": {}, + "all_lurker": {}, + "all_about_to_disengage": {}, + "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, + } + memberactivities = activity_dict.keys() + + act_param = { + "INT_THR": 1, + "UW_DEG_THR": 1, + "PAUSED_T_THR": 1, + "CON_T_THR": 4, + "CON_O_THR": 3, + "EDGE_STR_THR": 5, + "UW_THR_DEG_THR": 5, + "VITAL_T_THR": 4, + "VITAL_O_THR": 3, + "STILL_T_THR": 2, + "STILL_O_THR": 2, + "DROP_H_THR": 2, + "DROP_I_THR": 1, + } + + int_mat = { + DiscordActivity.Reply: np.zeros((acc_count, acc_count)), + DiscordActivity.Mention: np.zeros((acc_count, acc_count)), + DiscordActivity.Reaction: np.zeros((acc_count, acc_count)), + } + + # `user_1` intracting with `user_2` + int_mat[DiscordActivity.Reaction][0, 1] = 2 + + activities = [ + DiscordActivity.Reaction, + DiscordActivity.Mention, + DiscordActivity.Reply, + ] + + engagement = EngagementAssessment( + activities=activities, activities_ignore_0_axis=[], activities_ignore_1_axis=[] + ) + + # the analytics + for w_i in range(max_interval): + # time window + WINDOW_D = 7 + + (_, *activity_dict) = engagement.compute( + int_mat=int_mat, + w_i=w_i, + acc_names=acc_names, + act_param=act_param, + WINDOW_D=WINDOW_D, + **activity_dict, + ) + if w_i == 14: + int_mat[DiscordActivity.Reaction][0, 1] = 0 + + activity_dict = dict(zip(memberactivities, activity_dict)) + + print("all_consistent:", activity_dict["all_consistent"]) + print("all_paused:", activity_dict["all_paused"]) + print("all_inconsistent:", activity_dict["all_inconsistent"]) + + assert activity_dict["all_inconsistent"] == { + "0": set(), + "1": set(), + "2": set(), + "3": set(), + "4": set(), + "5": set(), + "6": set(), + "7": {"user0", "user1"}, + "8": {"user0", "user1"}, + "9": {"user0", "user1"}, + "10": {"user0", "user1"}, + "11": {"user0", "user1"}, + "12": {"user0", "user1"}, + "13": {"user0", "user1"}, + "14": set(), + "15": {"user0", "user1"}, + "16": {"user0", "user1"}, + "17": {"user0", "user1"}, + "18": {"user0", "user1"}, + "19": {"user0", "user1"}, + "20": {"user0", "user1"}, + "21": set(), + "22": set(), + "23": set(), + "24": set(), + "25": set(), + "26": set(), + "27": set(), + } diff --git a/tc_core_analyzer_lib/tests/integration/test_mention_active_members_from_int_matrix.py b/tc_core_analyzer_lib/tests/integration/test_mention_active_members_from_int_matrix.py index 9601772..3341875 100644 --- a/tc_core_analyzer_lib/tests/integration/test_mention_active_members_from_int_matrix.py +++ b/tc_core_analyzer_lib/tests/integration/test_mention_active_members_from_int_matrix.py @@ -62,6 +62,11 @@ def test_mention_active_members_from_int_matrix(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_newly_active_continuous_period.py b/tc_core_analyzer_lib/tests/integration/test_newly_active_continuous_period.py index d1d2e39..7afde8a 100644 --- a/tc_core_analyzer_lib/tests/integration/test_newly_active_continuous_period.py +++ b/tc_core_analyzer_lib/tests/integration/test_newly_active_continuous_period.py @@ -43,6 +43,11 @@ def test_newly_active_continuous_period(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_newly_active_discontinued_period.py b/tc_core_analyzer_lib/tests/integration/test_newly_active_discontinued_period.py index 3a9826f..8f48a59 100644 --- a/tc_core_analyzer_lib/tests/integration/test_newly_active_discontinued_period.py +++ b/tc_core_analyzer_lib/tests/integration/test_newly_active_discontinued_period.py @@ -57,6 +57,11 @@ def test_newly_active_discontinued_period(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_non_consistently_active.py b/tc_core_analyzer_lib/tests/integration/test_non_consistently_active.py index 44494a6..a299cfe 100644 --- a/tc_core_analyzer_lib/tests/integration/test_non_consistently_active.py +++ b/tc_core_analyzer_lib/tests/integration/test_non_consistently_active.py @@ -45,6 +45,11 @@ def test_two_consistently_active_non(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_partially_consistently_active.py b/tc_core_analyzer_lib/tests/integration/test_partially_consistently_active.py index 0b5ee3a..7d1c140 100644 --- a/tc_core_analyzer_lib/tests/integration/test_partially_consistently_active.py +++ b/tc_core_analyzer_lib/tests/integration/test_partially_consistently_active.py @@ -43,6 +43,11 @@ def test_two_consistently_active_partially(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_still_active.py b/tc_core_analyzer_lib/tests/integration/test_still_active.py index 56a3768..1fb53ef 100644 --- a/tc_core_analyzer_lib/tests/integration/test_still_active.py +++ b/tc_core_analyzer_lib/tests/integration/test_still_active.py @@ -58,6 +58,11 @@ def test_still_active_members(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivities = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/integration/test_vital.py b/tc_core_analyzer_lib/tests/integration/test_vital.py index 01d426c..bf5ffd9 100644 --- a/tc_core_analyzer_lib/tests/integration/test_vital.py +++ b/tc_core_analyzer_lib/tests/integration/test_vital.py @@ -58,6 +58,11 @@ def test_one_vital(): "all_lurker": {}, "all_about_to_disengage": {}, "all_disengaged_in_past": {}, + "all_inconsistent": {}, + "all_new_consistent": {}, + "all_new_vital": {}, + "all_became_not_consistent": {}, + "all_became_unvital": {}, } memberactivites = activity_dict.keys() diff --git a/tc_core_analyzer_lib/tests/unit/test_assess_inconsistent.py b/tc_core_analyzer_lib/tests/unit/test_assess_inconsistent.py new file mode 100644 index 0000000..27a6704 --- /dev/null +++ b/tc_core_analyzer_lib/tests/unit/test_assess_inconsistent.py @@ -0,0 +1,58 @@ +import unittest + +from tc_core_analyzer_lib.utils.assessments.assess_inconsistent import ( + assess_inconsistent, +) + + +class TestAssessInconsistent(unittest.TestCase): + def test_inconsistent_members_empty_inputs(self): + all_active = {"0": set()} + all_paused = {"0": set()} + all_new_active = {"0": set()} + all_consistent = {"0": set()} + + all_inconsistent = assess_inconsistent( + all_active=all_active, + all_paused=all_paused, + all_new_active=all_new_active, + all_consistent=all_consistent, + w_i=0, + ) + + self.assertEqual(all_inconsistent, set()) + + def test_inconsistent_members_paused_members(self): + all_active = {"0": set(["User1", "User2"])} + all_paused = {"0": set(["User3", "User4"])} + all_new_active = {"0": set(["User1", "User2"])} + all_consistent = {"0": set()} + + all_inconsistent = assess_inconsistent( + all_active=all_active, + all_paused=all_paused, + all_new_active=all_new_active, + all_consistent=all_consistent, + w_i=0, + ) + + self.assertEqual(all_inconsistent, set(["User3", "User4"])) + + def test_inconsistent_members_active_members(self): + all_active = {"0": set(["User1", "User2"]), "1": set(["User1", "User2"])} + all_paused = {"0": set(["User3", "User4"]), "1": set()} + all_new_active = { + "0": set(["User1", "User2"]), + "1": set([]), + } + all_consistent = {"0": set(), "1": set()} + + all_inconsistent = assess_inconsistent( + all_active=all_active, + all_paused=all_paused, + all_new_active=all_new_active, + all_consistent=all_consistent, + w_i=1, + ) + + self.assertEqual(all_inconsistent, set(["User1", "User2"])) diff --git a/tc_core_analyzer_lib/utils/assessments/__init__.py b/tc_core_analyzer_lib/utils/assessments/__init__.py index b5ee417..8e7c7e7 100644 --- a/tc_core_analyzer_lib/utils/assessments/__init__.py +++ b/tc_core_analyzer_lib/utils/assessments/__init__.py @@ -3,6 +3,7 @@ from .assess_connected import assess_connected from .assess_consistent import assess_consistent from .assess_dropped import assess_dropped +from .assess_inconsistent import assess_inconsistent from .assess_lurker import assess_lurker from .assess_overlap import assess_overlap from .assess_remainder import assess_remainder diff --git a/tc_core_analyzer_lib/utils/assessments/assess_inconsistent.py b/tc_core_analyzer_lib/utils/assessments/assess_inconsistent.py new file mode 100644 index 0000000..a1f953e --- /dev/null +++ b/tc_core_analyzer_lib/utils/assessments/assess_inconsistent.py @@ -0,0 +1,39 @@ +def assess_inconsistent( + all_active: dict[str, set[str]], + all_paused: dict[str, set[str]], + all_new_active: dict[str, set[str]], + all_consistent: dict[str, set[str]], + w_i: int, +) -> set[str]: + """ + assess inconsistently active members base on the given input sets + + Parameters + ------------ + all_active : dict[str, set[str]] + dictionary with keys w_i and values + containing a set of all account names that are active + all_paused : dict[str, set[str]] + dictionary with keys w_i and values + containing a set of all account names that are paused + all_new_active : dict[str, set[str]] + dictionary with keys w_i and values + containing a set of all account names that are active for first time + all_consistent : dict[str, set[str]] + dictionary with keys w_i and values + containing a set of all account names that are consistently active + w_i : int + index of sliding time window + + Returns + --------- + all_inconsistent : set[str] + containing a set of all account names that are inconsistently active + """ + all_inconsistent = ( + all_active[str(w_i)].union(all_paused[str(w_i)]) + - all_new_active[str(w_i)] + - all_consistent[str(w_i)] + ) + + return all_inconsistent