fullscript.spellbook 2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ '''
2
+ What this script does:
3
+
4
+ https://www.thepythoncode.com/article/using-google-drive--api-in-python
5
+ https://developers.google.com/drive/api/v3/reference/files
6
+ '''
7
+
8
+ # importing packages
9
+ import os
10
+ import io
11
+ import pandas as pd
12
+ import base64
13
+ import pickle
14
+ from googleapiclient.discovery import build
15
+ from google_auth_oauthlib.flow import InstalledAppFlow
16
+ from google.auth.transport.requests import Request
17
+ from googleapiclient.http import MediaFileUpload
18
+ from googleapiclient.http import MediaIoBaseDownload
19
+ from googleapiclient.errors import HttpError
20
+ from google.oauth2 import service_account
21
+ from spellbook.utils import load_and_parse_config
22
+
23
+ '''
24
+ Authentication Process
25
+ '''
26
+
27
+
28
+ def get_service(scopes, service_account_name, auth_type='service_account', **kwargs):
29
+ """Gets Google Drive service
30
+ Parameters
31
+ ----------
32
+ scopes : list
33
+ List of strings representing the scopes one needs service for.
34
+ Example: ['https://www.googleapis.com/auth/drive.metadata.readonly',
35
+ 'https://www.googleapis.com/auth/drive.file']
36
+ auth_type : str
37
+ Authorization type selected. It can be 'service_account' or 'oauth'
38
+
39
+ Returns
40
+ -------
41
+ resource
42
+ Google Drive service object
43
+ :param scopes: The scopes needed for the service
44
+ :param service_account_name: The name of the Google Sheets service account to be used for authorization
45
+ """
46
+ try:
47
+ if auth_type == 'service_account':
48
+ # option that uses the credential process we already have for sheets_magic
49
+ # this method requires a folder to be shared with the service account email (as we do with our sheets)
50
+
51
+ # Load the configuration
52
+ config = load_and_parse_config(configurations='google_accounts', **kwargs)
53
+ gs_service_account = None
54
+
55
+ # Find the Google Sheets service account details in the configuration
56
+ google_accounts = config.get('google_accounts', [])
57
+ for gs_account in google_accounts:
58
+ if gs_account.get('name') == service_account_name:
59
+ gs_service_account = gs_account.get('service_account')
60
+ break
61
+
62
+ if not gs_service_account:
63
+ raise ValueError(
64
+ f"Google Sheets service account '{service_account_name}' not found in configuration file.")
65
+
66
+ # creates a credentials file using the info in the env var GS_SERVICE_ACCOUNT
67
+ with open('credentials.json', 'w') as credential_file:
68
+ print(base64.b64decode(os.environ['GS_SERVICE_ACCOUNT']).decode(), file=credential_file)
69
+
70
+ # uses the file to create the scoped credentials
71
+ credentials = service_account.Credentials.from_service_account_file('credentials.json')
72
+ scoped_credentials = credentials.with_scopes(scopes)
73
+ os.remove('credentials.json')
74
+
75
+ elif auth_type == 'oauth':
76
+ # This option will prompt the user to authorize the script to use their identity. This allows the script to read
77
+ # and write to any folder the user has access to.
78
+ # The file token.pickle stores the user's access and refresh tokens. It's automatically created at the first time
79
+ # the user completes the authorization.
80
+ # It's important to notice everytime a new scope is needed, the authorization needs to be run again.
81
+
82
+ # initializing scoped_credentials
83
+ scoped_credentials = None
84
+
85
+ # if the token.pickle already exists, we can get credentials from it
86
+ if os.path.exists('token.pickle'):
87
+ with open('token.pickle', 'rb') as token:
88
+ scoped_credentials = pickle.load(token)
89
+
90
+ # If the file doesn't exist or is expired, we need to run the authorization flow
91
+ if not scoped_credentials or not scoped_credentials.valid:
92
+
93
+ if scoped_credentials and scoped_credentials.expired and scoped_credentials.refresh_token:
94
+ # refresh the credentials
95
+ scoped_credentials.refresh(Request())
96
+ else:
97
+ # run the authorization flow from the client secrets stored as a env var
98
+ with open('client_secrets.json', 'w') as credential_file:
99
+ print(base64.b64decode(os.environ['GS_CLIENT_SECRETS']).decode(), file=credential_file)
100
+
101
+ flow = InstalledAppFlow.from_client_secrets_file('client_secrets.json', scopes)
102
+ scoped_credentials = flow.run_local_server(port=3000)
103
+
104
+ os.remove('client_secrets.json')
105
+
106
+ # save the credentials for the next run
107
+ with open('token.pickle', 'wb') as token:
108
+ pickle.dump(scoped_credentials, token)
109
+
110
+ # return the built service object.
111
+ return build('drive', 'v3', credentials=scoped_credentials)
112
+ except HttpError as error:
113
+ print(f'An error occurred: {error}')
114
+ return None
115
+
116
+
117
+ '''
118
+ Other functions
119
+ '''
120
+
121
+
122
+ def list_folder_contents(service, folder_url, max_results=1000, show_trashed=False):
123
+ """Gets files in a specific folder
124
+ Parameters
125
+ ----------
126
+ service : resource
127
+ Google Drive resource object
128
+ folder_url : str
129
+ Folder URL
130
+ max_results : int
131
+ Number of results to be returned
132
+ show_trashed : bool
133
+ Flag whether files in the trash should be returned
134
+
135
+ Returns
136
+ -------
137
+ pd.DataFrame
138
+ Dataframe with file data
139
+ """
140
+
141
+ # get the folder id
142
+ folder_id = folder_url.rsplit('/', 1)[-1]
143
+
144
+ try:
145
+ # get items from GDrive
146
+ results = service.files().list(q=f"'{folder_id}' in parents and trashed = {str(show_trashed).lower()}",
147
+ pageSize=max_results, orderBy='name',
148
+ includeItemsFromAllDrives=True, supportsAllDrives=True,
149
+ fields="nextPageToken, files(id, name, mimeType, size, parents, modifiedTime)").execute()
150
+ items = results.get('files', [])
151
+
152
+ # returns as a DF
153
+ df_temp = pd.DataFrame(items)
154
+ df_temp = df_temp.rename({'size': 'size_bytes'}, axis=1)
155
+ return df_temp
156
+ except HttpError as error:
157
+ print(f'An error occurred: {error}')
158
+ return None
159
+
160
+
161
+ def download_file(service, file_id, filepath):
162
+ """Downloads a file from Google Drive
163
+ Parameters
164
+ ----------
165
+ service : resource
166
+ Google Drive resource object
167
+ file_id : str
168
+ ID of the file to be downloaded
169
+ filepath : str
170
+ File path where the downloaded file should be saved. (i.e.: 'samples/test_file.txt')
171
+
172
+ Returns
173
+ -------
174
+ bool
175
+ Flag whether the file was successfully downloaded or not
176
+ """
177
+
178
+ try:
179
+ # pylint: disable=maybe-no-member
180
+ request = service.files().get_media(fileId=file_id, supportsAllDrives=True)
181
+ file = io.BytesIO()
182
+ downloader = MediaIoBaseDownload(file, request)
183
+ done = False
184
+ while done is False:
185
+ status, done = downloader.next_chunk()
186
+ print(F'Download {int(status.progress() * 100)}.')
187
+
188
+ with open(filepath, "wb") as f:
189
+ f.write(file.getbuffer())
190
+
191
+ return True
192
+
193
+ except HttpError as error:
194
+ print(F'An error occurred: {error}')
195
+ return False
196
+
197
+
198
+ def upload_file(service, folder_url, filepath):
199
+ """Gets files in a specific folder
200
+ Parameters
201
+ ----------
202
+ service : resource
203
+ Google Drive resource object
204
+ folder_url : str
205
+ Folder URL
206
+ filepath : str
207
+ Path of the file to be uploaded
208
+
209
+ Returns
210
+ -------
211
+ str
212
+ ID of the created file
213
+ """
214
+
215
+ # get the folder id
216
+ folder_id = folder_url.rsplit('/', 1)[-1]
217
+
218
+ # first, define file metadata, such as the name and the parent folder ID
219
+ filename = os.path.basename(filepath)
220
+ file_metadata = {
221
+ "name": filename,
222
+ "parents": [folder_id]
223
+ }
224
+ # upload
225
+ try:
226
+ media = MediaFileUpload(filepath, resumable=True)
227
+ file = service.files().create(body=file_metadata, media_body=media, fields='id',
228
+ supportsAllDrives=True).execute()
229
+ print(f'File created! - id: {file.get("id")}')
230
+ return file.get("id")
231
+ except HttpError as error:
232
+ print(f'An error occurred: {error}')
233
+ return None
234
+
235
+
236
+ def delete_file(service, file_id):
237
+ """Permanently deletes a file owned by the user without moving it to the trash. If the file belongs to a shared drive
238
+ the user must be an organizer on the parent. If the target is a folder, all descendants owned by the user are also
239
+ deleted. In case the delete method is unsuccessful, tries to move the file to trash.
240
+ Parameters
241
+ ----------
242
+ service : resource
243
+ Google Drive resource object
244
+ file_id : str
245
+ ID of the file to be deleted
246
+
247
+ Returns
248
+ -------
249
+ bool
250
+ Flag whether the file was successfuly deleted or not
251
+ """
252
+ try:
253
+ try:
254
+ # delete method
255
+ service.files().delete(fileId=file_id, supportsAllDrives=True).execute()
256
+ print('File deleted successfully!')
257
+ return True
258
+ except:
259
+ # move to trash method
260
+ body = {'trashed': True}
261
+ service.files().update(fileId=file_id, body=body, supportsAllDrives=True).execute()
262
+ print('Not able to delete the file. Moved to trash instead.')
263
+ return True
264
+ except HttpError as error:
265
+ print(f'An error occurred: {error}')
266
+ return False
@@ -0,0 +1,181 @@
1
+ import os
2
+ import pandas as pd
3
+ import pygsheets
4
+ import base64
5
+ from spellbook.utils import load_and_parse_config
6
+
7
+
8
+ class GSheetsHandler:
9
+ """
10
+ :param gsheet_url: The URL of the target Google Sheet workbook
11
+ :param service_account_name: The name of the Google Sheets service account to be used for authorization
12
+ """
13
+
14
+ def __init__(self, gsheet_url, service_account_name):
15
+ self._gsheet_url = gsheet_url
16
+ self._service_account_name = service_account_name
17
+ if os.path.isfile('spellbook_config.yaml'):
18
+ self._config_file = 'spellbook_config.yaml'
19
+ else:
20
+ self._config_file = os.path.expanduser('~')+'/'+'spellbook_config.yaml'
21
+ self._df_results = pd.DataFrame()
22
+ self._gc = None
23
+ self._sh = None
24
+
25
+ # Initialize Google Sheets authorization
26
+ self._authorize_google_sheets()
27
+
28
+ # Open the Google Spreadsheet using the URL
29
+ if self._gc:
30
+ self._sh = self._gc.open_by_url(self._gsheet_url)
31
+
32
+ def _authorize_google_sheets(self):
33
+ """
34
+ Private method to handle Google Sheets authorization using credentials from the configuration file.
35
+ Loads the service account credentials, decodes them, and uses them for authorization.
36
+ """
37
+ try:
38
+ # Load the configuration
39
+ config = load_and_parse_config(config_file_path=self._config_file, configurations='google_accounts')
40
+ gs_service_account = None
41
+
42
+ # Find the Google Sheets service account details in the configuration
43
+ google_accounts = config.get('google_accounts', [])
44
+ for gs_account in google_accounts:
45
+ if gs_account.get('name') == self._service_account_name:
46
+ gs_service_account = gs_account.get('service_account')
47
+ break
48
+
49
+ if not gs_service_account:
50
+ raise ValueError(f"Google Sheets service account '{self._service_account_name}' not found in configuration file.")
51
+
52
+ # Decode and write credentials from configuration to a temporary file
53
+ with open("credentials.json", "w") as credential_file:
54
+ print(base64.b64decode(gs_service_account).decode(), file=credential_file)
55
+
56
+ # Use the JSON file to authenticate on Google Sheets
57
+ self._gc = pygsheets.authorize(service_file="credentials.json")
58
+ os.remove("credentials.json")
59
+ except FileNotFoundError:
60
+ print("Config file not found. Please ensure 'spelbook_config.yaml' exists.")
61
+ except ValueError as e:
62
+ print(f'Error: {e}')
63
+ except Exception as e:
64
+ print(f'Could not load Google Sheets service account. Error: {e}')
65
+
66
+
67
+ def delete_sheet(self, worksheet):
68
+ """
69
+ Deletes the specified worksheet from the Google Sheet.
70
+ If the worksheet does not exist, an error message is printed.
71
+ :param worksheet: The name or index of the worksheet to be deleted
72
+ """
73
+ try:
74
+ if str(worksheet).isdigit():
75
+ # Delete sheet by index
76
+ self._sh.del_worksheet(self._sh[int(worksheet)])
77
+ else:
78
+ # Delete sheet by title
79
+ self._sh.del_worksheet(self._sh.worksheet_by_title(worksheet))
80
+ print('Tab deleted')
81
+ except pygsheets.WorksheetNotFound as error:
82
+ print(f'Error while deleting sheet: {error}')
83
+
84
+ def create_sheet(self, worksheet):
85
+ """
86
+ Creates a new worksheet in the Google Sheet with the specified name.
87
+ If a worksheet with the given name already exists, it will not create a duplicate.
88
+ :param worksheet: The name of the new worksheet to create
89
+ """
90
+ try:
91
+ self._sh.add_worksheet(str(worksheet))
92
+ except Exception as error:
93
+ print(f'Error while creating sheet: {error}')
94
+
95
+ def get_sheet(self, worksheet):
96
+ """
97
+ Retrieves the specified worksheet object from the Google Sheet.
98
+ The worksheet can be accessed either by its name or its index.
99
+ If the worksheet does not exist, an error message is printed.
100
+ :param worksheet: The name or index of the worksheet to retrieve
101
+ :return: The worksheet object or None if not found
102
+ """
103
+ try:
104
+ if str(worksheet).isdigit():
105
+ return self._sh[int(worksheet)]
106
+ else:
107
+ return self._sh.worksheet_by_title(worksheet)
108
+ except pygsheets.WorksheetNotFound:
109
+ print(f'Sheet does not exist')
110
+ return None
111
+
112
+ def count_rows(self, worksheet, target_column=1, include_tailing_empty=False):
113
+ """
114
+ Counts the number of rows in the specified target column of a worksheet.
115
+ :param worksheet: The name of the worksheet to read from
116
+ :param target_column: The column number to count rows in
117
+ :param include_tailing_empty: Whether to include empty trailing cells in the count
118
+ :return: The count of rows in the specified column
119
+ """
120
+ wks = self.get_sheet(worksheet)
121
+ if wks:
122
+ return len(wks.get_col(col=target_column, returnas='matrix', include_tailing_empty=include_tailing_empty))
123
+ return 0
124
+
125
+ def clear_range(self, worksheet, clear_range_str=None):
126
+ """
127
+ Clears the specified range or the entire worksheet in the Google Sheet.
128
+ :param worksheet: The name of the worksheet to clear
129
+ :param clear_range_str: The range to be cleared (e.g., 'A1:B2'). If None, the entire worksheet will be cleared
130
+ """
131
+ wks = self.get_sheet(worksheet)
132
+ if wks:
133
+ if clear_range_str:
134
+ clear_range = clear_range_str.split(':')
135
+ wks.clear(clear_range[0], clear_range[1])
136
+ else:
137
+ wks.clear()
138
+
139
+ def write_to_sheet(self, input_dataframe, worksheet, starting_pos='A1', extend=True, fit=False, nan='', clear_ws=True, clear_range_str=None):
140
+ """
141
+ Writes a pandas DataFrame to the specified worksheet in the Google Sheet.
142
+ If the worksheet does not exist, it will be created.
143
+ Optionally clears the worksheet or a specific range before writing.
144
+ :param input_dataframe: The pandas DataFrame to be written to the worksheet
145
+ :param worksheet: The worksheet to write to
146
+ :param starting_pos: The starting cell position to write the DataFrame (e.g., 'A1')
147
+ :param extend: Whether to extend the worksheet to fit the DataFrame if necessary
148
+ :param fit: Whether to resize the worksheet to fit the entire DataFrame
149
+ :param nan: The value to replace NaN entries with
150
+ :param clear_ws: Whether to clear the worksheet before writing the DataFrame
151
+ :param clear_range_str: The range to be cleared before writing if clear_ws is True
152
+ """
153
+ wks = self.get_sheet(worksheet)
154
+ if not wks:
155
+ print('Creating new sheet')
156
+ self.create_sheet(worksheet)
157
+ wks = self.get_sheet(worksheet)
158
+
159
+ if clear_ws and wks:
160
+ self.clear_range(worksheet=worksheet, clear_range_str=clear_range_str)
161
+
162
+ if wks:
163
+ wks.set_dataframe(df=input_dataframe, start=starting_pos, extend=extend, fit=fit, nan=nan)
164
+ print('Write to sheet completed')
165
+
166
+ def read_sheet(self, worksheet, target_range_str=None):
167
+ """
168
+ Reads data from the specified worksheet and returns it as a pandas DataFrame.
169
+ If the target range is provided, only that specific range is read.
170
+ :param worksheet: The name of the worksheet to read from
171
+ :param target_range_str: The target range to read from (e.g., 'A1:B2'). If None, the entire worksheet will be read
172
+ :return: A pandas DataFrame containing the data from the specified range or worksheet
173
+ """
174
+ wks = self.get_sheet(worksheet)
175
+ if wks:
176
+ if target_range_str:
177
+ target_range = target_range_str.split(':')
178
+ return wks.get_as_df(start=target_range[0], end=target_range[1])
179
+ else:
180
+ return wks.get_as_df()
181
+ return pd.DataFrame()
@@ -0,0 +1,211 @@
1
+ import scipy.stats as stats
2
+ import pandas as pd
3
+ from statsmodels.stats.multicomp import pairwise_tukeyhsd
4
+ import scikit_posthocs as sp
5
+
6
+ '''
7
+ Statistical tests
8
+ '''
9
+
10
+
11
+ def shapiro_wilk_normality_test(df, group_column, values_column, alpha_level = 0.05, verbose = False):
12
+ results = []
13
+
14
+ if verbose:
15
+ print('Shapiro-Wilk Normality Test - H0: Samples follow a normal distribution')
16
+
17
+ # getting list of groups
18
+ list_groups = list(set(df[group_column]))
19
+
20
+ for column in list_groups:
21
+ # p value of the test
22
+ p_shapiro = stats.shapiro(df[df[group_column] == column][values_column].dropna()).pvalue
23
+
24
+ # printing results
25
+ if p_shapiro < alpha_level:
26
+ result = 'H0 rejected - non-normal distribution'
27
+ else:
28
+ result = 'H0 cannot be rejected - normal distribution'
29
+
30
+ if verbose:
31
+ print('Shapiro-Wilk Normality Test for: {} - p = {:.3f} - Result: {}'.format(column, p_shapiro, result))
32
+
33
+ results.append([p_shapiro, result])
34
+
35
+ return pd.DataFrame(results, columns=['p_value', 'result'])
36
+
37
+
38
+ def levene_variance_test(df, group_column, values_column, alpha_level = 0.05, center_method = 'mean', verbose = False):
39
+ '''
40
+ Three variations of Levene’s test are possible. The possibilities and their recommended usages are:
41
+ ‘median’ : Recommended for skewed (non-normal) distributions
42
+ ‘mean’ : Recommended for symmetric, moderate-tailed distributions.
43
+ ‘trimmed’ : Recommended for heavy-tailed distributions.
44
+ '''
45
+ # getting list of groups
46
+ list_groups = list(set(df[group_column]))
47
+ list_of_lists = [df[df[group_column] == column][values_column].dropna() for column in list_groups]
48
+
49
+ results = []
50
+
51
+ if verbose:
52
+ print("Levene's Variance Test - H0: Samples come from populations with equal variances")
53
+
54
+ stat_levene, p_levene = stats.levene(*list_of_lists, center=center_method)
55
+
56
+ if p_levene < alpha_level:
57
+ result = 'H0 rejected - different variances'
58
+ else:
59
+ result = 'H0 cannot be rejected - same variance'
60
+
61
+ if verbose:
62
+ print('Levene Variance Test: p = {:.3f} - Result: {}'.format(p_levene, result))
63
+
64
+ results.append([p_levene, result])
65
+
66
+ return pd.DataFrame(results, columns=['p_value', 'result'])
67
+
68
+
69
+ def anova(df, group_column, values_column, alpha_level = 0.05, verbose = False):
70
+ # getting list of groups
71
+ list_groups = list(set(df[group_column]))
72
+ list_of_lists = [df[df[group_column] == column][values_column].dropna() for column in list_groups]
73
+
74
+ results = []
75
+
76
+ # for normal data, One Way ANOVA
77
+ f_anova, p_anova = stats.f_oneway(*list_of_lists)
78
+
79
+ if p_anova < alpha_level:
80
+ result = 'H0 rejected - The average values have statistically significant differences.'
81
+ else:
82
+ result = 'H0 cannot be rejected - The average values have NOT statistically significant differences.'
83
+
84
+ if verbose:
85
+ print("ANOVA - H0: Average values are the same")
86
+ print('F statistic = {:5.3f} and probability p = {:5.3f}'.format(f_anova, p_anova))
87
+ print('Result: {}'.format(result))
88
+
89
+ results.append([f_anova, p_anova, result])
90
+
91
+ return pd.DataFrame(results, columns=['f_statistic', 'p_value', 'result'])
92
+
93
+
94
+ def tukey_test(df, group_column, values_column, alpha_level = 0.05, verbose = False):
95
+ tukey_table = pairwise_tukeyhsd(endog=df[values_column], groups=df[group_column], alpha=alpha_level)
96
+ if verbose:
97
+ print("Tukey's Test - H0: No significant difference has been observed for the pair")
98
+ print(tukey_table.summary())
99
+
100
+ return tukey_table
101
+
102
+
103
+ def mood_median_test(df, group_column, values_column, alpha_level = 0.05, verbose = False):
104
+ # getting list of groups
105
+ list_groups = list(set(df[group_column]))
106
+ list_of_lists = [df[df[group_column] == column][values_column].dropna() for column in list_groups]
107
+
108
+ results = []
109
+
110
+ # for non-normal data, Mood's Median Test
111
+ f_mood, p_mood, med_mood, tbl_mood = stats.median_test(*list_of_lists)
112
+
113
+ if p_mood < alpha_level:
114
+ result = 'H0 rejected - The median values have statistically significant differences.'
115
+ else:
116
+ result = 'H0 cannot be rejected - The median values have NOT statistically significant differences.'
117
+
118
+ if verbose:
119
+ print("Mood's Median Test - H0: Median values are the same")
120
+ print("F statistic = {:5.3f} and probability p = {:5.3f}".format(f_mood, p_mood))
121
+ print('Result: {}'.format(result))
122
+
123
+ results.append([f_mood, p_mood, result])
124
+
125
+ return pd.DataFrame(results, columns=['f_statistic', 'p_value', 'result'])
126
+
127
+
128
+ def dunn_test(df, group_column, values_column, alpha_level = 0.05, verbose = False):
129
+ # Dunn's test to check where is the difference
130
+ dunn_results = sp.posthoc_dunn(
131
+ a=df
132
+ , val_col=values_column
133
+ , group_col=group_column
134
+ , p_adjust='bonferroni'
135
+ )
136
+
137
+ # clean retults
138
+ list_groups = list(set(df[group_column]))
139
+ results = [[a, b, dunn_results[a][b]] for idx, a in enumerate(list_groups) for b in list_groups[idx + 1:]]
140
+
141
+ results = pd.DataFrame(results, columns=['a', 'b', 'p_value'])
142
+
143
+ results['reject'] = (results['p_value'] < alpha_level)
144
+
145
+ if verbose:
146
+ print("Dunn's Test - H0: No significant difference has been observed for the pair")
147
+ print(results)
148
+
149
+ return results
150
+
151
+
152
+ def t_welch_test(df, group_column, values_column, equal_variances = True, alpha_level = 0.05, verbose = False):
153
+ # getting list of groups
154
+ list_groups = list(set(df[group_column]))
155
+
156
+ if len(list_groups) == 2:
157
+
158
+ results = []
159
+
160
+ list_of_lists = [df[df[group_column] == column][values_column].dropna() for column in list_groups]
161
+
162
+ # T test for same variance / Welch’s t-test for different variances
163
+ f_ttest, p_ttest = stats.ttest_ind(*list_of_lists, equal_var=equal_variances)
164
+
165
+ if p_ttest < alpha_level:
166
+ result = 'H0 rejected - The average values have statistically significant differences.'
167
+ else:
168
+ result = 'H0 cannot be rejected - The average values have NOT statistically significant differences.'
169
+
170
+ if verbose:
171
+ print("T-Test / Welch's T-Test - H0: Averages values are the same")
172
+ print("F-statistic = {:5.3f} and p-value = {:5.3f}".format(f_ttest, p_ttest))
173
+ print('Result: {}'.format(result))
174
+
175
+ results.append([f_ttest, p_ttest, result])
176
+
177
+ return pd.DataFrame(results, columns=['f_statistic', 'p_value', 'result'])
178
+
179
+ else:
180
+ print("T-Test / Welch's T-Test: Error: This functions requires exactly 2 groups")
181
+
182
+
183
+ def mann_whitney_u_test(df, group_column, values_column, alpha_level = 0.05, verbose = False):
184
+ # getting list of groups
185
+ list_groups = list(set(df[group_column]))
186
+
187
+ if len(list_groups) == 2:
188
+
189
+ results = []
190
+
191
+ list_of_lists = [df[df[group_column] == column][values_column].dropna() for column in list_groups]
192
+
193
+ # performs the test
194
+ stat_mann, p_mann = stats.mannwhitneyu(*list_of_lists)
195
+
196
+ if p_mann < alpha_level:
197
+ result = 'H0 rejected - Different distributions.'
198
+ else:
199
+ result = 'H0 cannot be rejected - Same distribution.'
200
+
201
+ if verbose:
202
+ print("Mann-Whitney U Test - H0: Same Distributions")
203
+ print("Statistic = {:5.3f} and p-value = {:5.3f}".format(stat_mann, p_mann))
204
+ print('Result: {}'.format(result))
205
+
206
+ results.append([stat_mann, p_mann, result])
207
+
208
+ return pd.DataFrame(results, columns=['statistic', 'p_value', 'result'])
209
+
210
+ else:
211
+ print("Mann-Whitney U Test: Error: This functions requires exactly 2 groups")