SuperModelingFactory 0.2.0__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.
Files changed (79) hide show
  1. ExcelMaster/ExcelFormatTool.py +487 -0
  2. ExcelMaster/ExcelMaster.py +917 -0
  3. ExcelMaster/Template.py +525 -0
  4. ExcelMaster/Utility.py +233 -0
  5. ExcelMaster/__init__.py +3 -0
  6. Modeling_Tool/Core/Binning_Tool.py +1608 -0
  7. Modeling_Tool/Core/Binning_Tool.pyi +48 -0
  8. Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
  9. Modeling_Tool/Core/Json_Data_Converter.py +621 -0
  10. Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
  11. Modeling_Tool/Core/ODPS_Tool.py +284 -0
  12. Modeling_Tool/Core/Slope_Tool.py +356 -0
  13. Modeling_Tool/Core/Slope_Tool.pyi +31 -0
  14. Modeling_Tool/Core/XOR_Encryptor.py +207 -0
  15. Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
  16. Modeling_Tool/Core/__init__.py +99 -0
  17. Modeling_Tool/Core/kDataFrame.py +228 -0
  18. Modeling_Tool/Core/kDataFrame.pyi +43 -0
  19. Modeling_Tool/Core/sample_weight_utils.py +77 -0
  20. Modeling_Tool/Core/utils.py +2672 -0
  21. Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
  22. Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
  23. Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
  24. Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
  25. Modeling_Tool/Eval/__init__.py +54 -0
  26. Modeling_Tool/Eval/evaluate_model.py +2008 -0
  27. Modeling_Tool/Eval/evaluate_model.pyi +50 -0
  28. Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
  29. Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
  30. Modeling_Tool/Explainability/Model_Explainer.py +743 -0
  31. Modeling_Tool/Explainability/__init__.py +24 -0
  32. Modeling_Tool/Feature/Distribution_Tool.py +509 -0
  33. Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
  34. Modeling_Tool/Feature/Feature_Insights.py +762 -0
  35. Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
  36. Modeling_Tool/Feature/PSI_Tool.py +1195 -0
  37. Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
  38. Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
  39. Modeling_Tool/Feature/__init__.py +40 -0
  40. Modeling_Tool/Model/Backward_Tool.py +778 -0
  41. Modeling_Tool/Model/Backward_Tool.pyi +45 -0
  42. Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
  43. Modeling_Tool/Model/GBM_Tool.py +1610 -0
  44. Modeling_Tool/Model/GBM_Tool.pyi +90 -0
  45. Modeling_Tool/Model/LRM_Tool.py +1198 -0
  46. Modeling_Tool/Model/LRM_Tool.pyi +47 -0
  47. Modeling_Tool/Model/__init__.py +61 -0
  48. Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
  49. Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
  50. Modeling_Tool/Sample/Reject_Infer.py +413 -0
  51. Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
  52. Modeling_Tool/Sample/Sample_Split.py +520 -0
  53. Modeling_Tool/Sample/Sample_Split.pyi +43 -0
  54. Modeling_Tool/Sample/__init__.py +31 -0
  55. Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
  56. Modeling_Tool/UAT/__init__.py +19 -0
  57. Modeling_Tool/WOE/WOE_Adapter.py +204 -0
  58. Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
  59. Modeling_Tool/WOE/WOE_Master.py +491 -0
  60. Modeling_Tool/WOE/WOE_Master.pyi +40 -0
  61. Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
  62. Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
  63. Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
  64. Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
  65. Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
  66. Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
  67. Modeling_Tool/WOE/WOE_Tool.py +1094 -0
  68. Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
  69. Modeling_Tool/WOE/__init__.py +86 -0
  70. Modeling_Tool/WOE/plot_woe_tool.py +290 -0
  71. Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
  72. Modeling_Tool/__init__.py +176 -0
  73. Report/Report_Tool.py +380 -0
  74. Report/__init__.py +3 -0
  75. supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
  76. supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
  77. supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
  78. supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
  79. supermodelingfactory-0.2.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,917 @@
1
+ __Author__ = "Jingkai SUN"
2
+ __Date__ = "2025.05.15"
3
+
4
+ import xlsxwriter
5
+ from xlsxwriter.utility import xl_rowcol_to_cell, xl_range, xl_cell_to_rowcol
6
+ import openpyxl
7
+ import pandas as pd
8
+ import numpy as np
9
+ from datetime import datetime
10
+ import logging
11
+ import warnings
12
+
13
+ from .ExcelFormatTool import ExcelFormat
14
+ from .Utility import *
15
+
16
+ class ExcelWorkbook(ExcelFormat):
17
+ """ Write anything to Excel (Workbook-level Operator). """
18
+ def __init__(self, filepath, verbose = True):
19
+ super().__init__(filepath)
20
+ self.verbose = verbose
21
+ self.ws_dict = {}
22
+
23
+ def to_cell_range_text(self, first_row, first_col, last_row, last_col):
24
+ """ To Excel Cell Range. """
25
+ return xl_range(first_row, first_col, last_row, last_col)
26
+
27
+ def cell_range_to_loc(self, cell_range_text):
28
+ """ Convert text cell range expression to a list of values. """
29
+ cell_range_list = cell_range_text.split(":")
30
+ res = []
31
+ for x in cell_range_list:
32
+ res += list(xl_cell_to_rowcol(x))
33
+ return res
34
+
35
+ def colletter_to_textloc(self, row_index, col_letter):
36
+ """ Append Row Index to the given Column Letter-Formatted Index. """
37
+ col_letter = col_letter if ":" in col_letter else (col_letter + ":" + col_letter)
38
+ output = col_letter.split(":")
39
+ output = ":".join([col + str(row_index) for col in output])
40
+ return output
41
+
42
+ def set_color_scale(self, worksheet, cell_range,
43
+ colors = ("#F8696B", "#FFEB84", "#63BE7B")):
44
+ """ Set Color Scale. """
45
+ if len(colors) == 3:
46
+ f = {
47
+ "type": "3_color_scale",
48
+ "min_color": colors[0],
49
+ "mid_color": colors[1],
50
+ "max_color": colors[2],
51
+ }
52
+ elif len(colors) == 2:
53
+ f = {
54
+ "type": "2_color_scale",
55
+ "min_color": colors[0],
56
+ "max_color": colors[1],
57
+ }
58
+ else:
59
+ raise ValueError("Please give 2 or 3 colors!")
60
+
61
+ if isinstance(cell_range, list) or isinstance(cell_range, tuple):
62
+ cell_range = self.to_cell_range_text(*cell_range)
63
+
64
+ worksheet.conditional_format(cell_range, f)
65
+ return 0
66
+
67
+ def set_data_bar(self, worksheet, cell_range, bar_color = "#63C384"):
68
+ """ Set Data Bar in a Worksheet for a range of cells. """
69
+
70
+ f = {'type': 'data_bar',
71
+ 'data_bar_2010': True,
72
+ "bar_color": bar_color}
73
+
74
+ if isinstance(cell_range, list) or isinstance(cell_range, tuple):
75
+ cell_range = self.to_cell_range_text(*cell_range)
76
+
77
+ worksheet.conditional_format(cell_range, f)
78
+ return 0
79
+
80
+ def set_cell_format(self, worksheet, cell_range, cformat, cell_condition = None):
81
+ """ Set format for a range of cell. """
82
+
83
+ if cell_condition and isinstance(cell_condition, tuple):
84
+ criteria = cell_condition[0].lower()
85
+ value = cell_condition[1]
86
+
87
+ if isinstance(cell_range, list) or isinstance(cell_range, tuple):
88
+ cell_range = self.to_cell_range_text(*cell_range)
89
+
90
+ if isinstance(cformat, str):
91
+ cformat = self.dict_cell_format[cformat]
92
+
93
+ if cell_condition and isinstance(cell_condition, tuple):
94
+
95
+ if (criteria == 'between') or (criteria == 'not between'):
96
+ worksheet.conditional_format(cell_range, {"type": "cell",
97
+ "criteria": criteria,
98
+ "minimum": value[0],
99
+ "maximum": value[1],
100
+ "format": cformat})
101
+
102
+ return 0
103
+
104
+ worksheet.conditional_format(cell_range, {"type": "cell",
105
+ "criteria": criteria,
106
+ "value": value,
107
+ "format": cformat})
108
+ return 0
109
+
110
+ worksheet.conditional_format(cell_range, {'type': 'no_errors', "format": cformat})
111
+ return 0
112
+
113
+ def set_cell_format_rbyr(self, worksheet, start_row, condition_list, condition, ifelse_col, cformat = "YELLOW_BG"):
114
+ """ Conditionally Highlight Cells Row by Row. """
115
+
116
+ if_col = ifelse_col[0]
117
+ else_col = ifelse_col[1]
118
+
119
+ i = 0
120
+ row_concur = start_row
121
+ while i < len(condition_list):
122
+ target = condition_list[i]
123
+
124
+ if val_input_condition(target, condition):
125
+ if if_col:
126
+ self.set_cell_format(worksheet,
127
+ cell_range=self.colletter_to_textloc(row_concur + 1, if_col),
128
+ cformat=cformat)
129
+
130
+ else:
131
+ if else_col:
132
+ self.set_cell_format(worksheet,
133
+ cell_range=self.colletter_to_textloc(row_concur + 1, else_col),
134
+ cformat=cformat)
135
+
136
+ i += 1
137
+ row_concur = row_concur + 1
138
+ return 0
139
+
140
+ def remove_tmp_img(self, img_pattern = r".tmp_image_[0-9]+.png"):
141
+ """ Remove Temp Images. """
142
+ tmp_imgs = list_files(location = "./", pattern = img_pattern)
143
+ if len(tmp_imgs) > 0:
144
+ for f in tmp_imgs:
145
+ os.remove(f)
146
+ return 0
147
+ return
148
+
149
+ def close_workbook(self):
150
+ '''
151
+ Finish writing the contents of the workbook and close the file.
152
+ '''
153
+ self.workbook.close()
154
+ self.remove_tmp_img(img_pattern = r".tmp_image_[0-9]+.png")
155
+ logging.info("All temp images have been removed.")
156
+
157
+
158
+ class ExcelMaster(ExcelWorkbook):
159
+ """ Write anything to Excel (Worksheet-level Operator) """
160
+ def __init__(self, filepath, verbose, gap_number = 2, init_loc = (0, 0)):
161
+ super().__init__(filepath, verbose)
162
+ self.curr_row = init_loc[0]
163
+ self.curr_col = init_loc[1]
164
+ self.gap_number = gap_number + 1
165
+
166
+ self.default_row_height = 20
167
+ self.default_col_width = 64
168
+
169
+ self.max_nrows = 1048576
170
+ self.max_ncols = 16384
171
+
172
+ def add_worksheet(self, name, hide_grid = True, reset_loc = True, cell_scale = True, auto_fit = False, zoom_perc = 100, tab_color = None):
173
+ """ Add a worksheet. """
174
+ ws = self.workbook.add_worksheet(name)
175
+
176
+ if hide_grid:
177
+ ws.hide_gridlines(2)
178
+
179
+ if reset_loc:
180
+ self.reset_curr_loc()
181
+
182
+ if isinstance(cell_scale, tuple):
183
+ self.set_cell_size(ws, cell_scale)
184
+ if cell_scale is True:
185
+ self.set_cell_size(ws)
186
+
187
+ if auto_fit:
188
+ ws.auto_fit()
189
+
190
+ if tab_color:
191
+ ws.set_tab_color(tab_color)
192
+
193
+ ws.set_zoom(zoom_perc)
194
+ self.ws_dict[name] = ws
195
+ self.engine.sheets[name] = ws
196
+ return ws
197
+
198
+ def reset_curr_loc(self, loc = (0, 0)):
199
+ """ Reset Current Location. """
200
+
201
+ self.curr_row = loc[0]
202
+ self.curr_col = loc[1]
203
+ return 0
204
+
205
+ def _reset_cell_size(self):
206
+ """ Reset Cell Size to Default Value. """
207
+ self.default_row_height = 20
208
+ self.default_col_width = 64
209
+ return 0
210
+
211
+ def set_cell_size(self, worksheet, size_scale = (1, 1)):
212
+ """ Set Cell Size in Scale. """
213
+
214
+ if isinstance(size_scale, tuple) and len(size_scale) == 2:
215
+ self._reset_cell_size()
216
+ self.default_row_height = self.default_row_height * size_scale[0]
217
+ self.default_col_width = self.default_col_width * size_scale[1]
218
+
219
+ for i in range(0, self.max_nrows):
220
+ worksheet.set_row_pixels(i, height=self.default_row_height)
221
+
222
+ worksheet.set_column_pixels(0, self.max_ncols - 1, width=self.default_col_width)
223
+ return 0
224
+
225
+ def get_curr_loc(self, toCell = False):
226
+ """ Get Current Location in worksheet. """
227
+ if toCell:
228
+ return xl_rowcol_to_cell(self.curr_row, self.curr_col)
229
+ return (self.curr_row, self.curr_col)
230
+
231
+ def set_border_line(self, worksheet, valuerange, border_line = 1):
232
+ """ Set border line for a range of cells. """
233
+ border_fmt = self.workbook.add_format({'bottom': border_line, 'top': border_line, 'left': border_line, 'right': border_line})
234
+ self.set_cell_format(worksheet = worksheet, cell_range = valuerange, cformat = border_fmt)
235
+ return 0
236
+
237
+ def merge_col(self, worksheet, loc = None, nrows = 1, ncols = 1, text = "", skipby = 'row', cformat = 'BLUE_H4', retCellRange = None):
238
+ """ Merge columns in a single row. """
239
+
240
+ start_row = loc[0] if loc else self.curr_row
241
+ start_col = loc[1] if loc else self.curr_col
242
+ written_range = [start_row, start_col, start_row + nrows - 1, start_col + ncols - 1]
243
+
244
+ worksheet.merge_range(*written_range, text, self.dict_cell_format[cformat])
245
+
246
+ if self.verbose:
247
+ logging.info(f"Merged Cells: {self.to_cell_range_text(*written_range)}")
248
+
249
+ # Skipped by Rows/Columns
250
+ if skipby == 'row':
251
+ self.curr_row = (start_row + nrows + self.gap_number)
252
+ if skipby == 'col':
253
+ self.curr_col = (start_col + ncols + self.gap_number)
254
+
255
+ # Return written location by Cell Text/Value Range
256
+ if retCellRange == "text":
257
+ return self.to_cell_range_text(*written_range)
258
+ if retCellRange == "value":
259
+ return written_range
260
+
261
+ return 0
262
+
263
+ def write_dataframe(self, worksheet, df, loc = None, title = None, index = False, header = True, skipby = 'row', titleformat = "BLUE_H4", headerformat = "TABLE_HEADER", valueformat="----", retCellRange = None):
264
+ """ Write a dataframe to excel file. """
265
+
266
+ start_row = loc[0] if loc else self.curr_row
267
+ start_col = loc[1] if loc else self.curr_col
268
+
269
+ ncols = df.shape[1]
270
+ nrows = df.shape[0]
271
+
272
+ ## Get Number of Index Columns
273
+ index_ncols = 0
274
+ if index:
275
+ index_ncols = len(df.index.names)
276
+ ncols += index_ncols
277
+
278
+ ## Get Number of Header Rows
279
+ header_nrows = 0
280
+ if header:
281
+ header_nrows = len(df.columns.names)
282
+ nrows += header_nrows
283
+
284
+ ## Get Number of Title Rows
285
+ title_nrows = 0
286
+ if title:
287
+ title_nrows = 1
288
+ nrows += title_nrows
289
+
290
+ ## Get Header Range (Include Index)
291
+ if header:
292
+ header_start_row = (start_row + title_nrows)
293
+ header_start_col = start_col
294
+ header_end_row = max((start_row + header_nrows - 1), header_start_row)
295
+ header_end_col = (start_col + ncols - 1)
296
+ header_range = self.to_cell_range_text(header_start_row, header_start_col,
297
+ header_end_row, header_end_col)
298
+
299
+ ## Write DataFrame
300
+ if title:
301
+ self.merge_col(worksheet = worksheet, loc = loc, ncols = ncols, cformat=titleformat,
302
+ text = title, skipby = None)
303
+ df.to_excel(self.engine, sheet_name = worksheet.name, startrow = start_row + title_nrows,
304
+ startcol = start_col, header = header, index = index)
305
+ else:
306
+ df.to_excel(self.engine, sheet_name = worksheet.name, startrow = start_row,
307
+ startcol = start_col, header = header, index = index)
308
+
309
+
310
+ ## Get Value Range (Include Index)
311
+ value_start_row = (start_row + title_nrows + header_nrows)
312
+ value_start_col = start_col
313
+ value_end_row = (start_row + nrows - 1)
314
+ value_end_col = (start_col + ncols - 1)
315
+ value_range = [value_start_row, value_start_col, value_end_row, value_end_col]
316
+
317
+ ## Set Format
318
+ self.set_cell_format(worksheet = worksheet, cell_range = value_range, cformat = valueformat)
319
+ if header:
320
+ self.set_cell_format(worksheet = worksheet, cell_range = header_range, cformat = headerformat)
321
+
322
+ written_range = [start_row, start_col, (start_row + nrows - 1), (start_col + ncols - 1)]
323
+
324
+ if self.verbose:
325
+ logging.info(f"Table Written in Cell Range: {self.to_cell_range_text(*written_range)}")
326
+
327
+ if skipby == 'row':
328
+ self.curr_row = (start_row + nrows + self.gap_number)
329
+ if skipby == 'col':
330
+ self.curr_col = (start_col + ncols + self.gap_number)
331
+
332
+ if retCellRange == "text":
333
+ return self.to_cell_range_text(*written_range)
334
+ if retCellRange == "value":
335
+ return written_range
336
+
337
+ return 0
338
+
339
+ def _get_image_size(self, figPath, figScale = (1, 1), retSizeInCell = True):
340
+ """ Get Image Size in Excel Cells. """
341
+ img = Image.open(figPath)
342
+ w, h = img.size
343
+ (width, height) = (img.width * figScale[0], img.height * figScale[1])
344
+ h_in_cell = np.floor(height/self.default_row_height)
345
+ w_in_cell = np.ceil(width/self.default_col_width)
346
+ img.close()
347
+ if retSizeInCell:
348
+ return (w_in_cell, h_in_cell)
349
+ return (width, height)
350
+
351
+ def _resize_image(self, imgPath, resize, outPath, size_in_cell = True):
352
+ """ Resize image. """
353
+ # Open the image
354
+ image = Image.open(imgPath)
355
+ new_image = image.resize((resize[0], resize[1]))
356
+ if size_in_cell:
357
+ # Resize the image
358
+ new_image = image.resize((resize[1] * self.default_col_width, resize[0] * self.default_row_height))
359
+ # Save the resized image
360
+ new_image.save(outPath)
361
+ image.close()
362
+ return 0
363
+
364
+ def insert_image(self, worksheet, figPath, figScale = (1, 1), loc = None, skipby = 'row', retCellRange = None):
365
+ """ Insert an image to the sheet. """
366
+ start_row = loc[0] if loc else self.curr_row
367
+ start_col = loc[1] if loc else self.curr_col
368
+
369
+ start_cell = xl_rowcol_to_cell(start_row, start_col)
370
+ figsize_in_cells = self._get_image_size(figPath = figPath, figScale = figScale, retSizeInCell = True)
371
+
372
+ worksheet.insert_image(start_cell, figPath, {"x_scale": figScale[0], "y_scale": figScale[1]})
373
+
374
+ # Skipped by Rows/Columns
375
+ if skipby == 'row':
376
+ self.curr_row = (start_row + int(figsize_in_cells[1]) + self.gap_number)
377
+ if skipby == 'col':
378
+ self.curr_col = (start_col + int(figsize_in_cells[0]) + self.gap_number)
379
+
380
+ written_range = [start_row, start_col,
381
+ start_row + int(figsize_in_cells[1]),
382
+ start_col + int(figsize_in_cells[0])]
383
+
384
+ # Return written location by Cell Text/Value Range
385
+ if retCellRange == "text":
386
+ return self.to_cell_range_text(*written_range)
387
+ if retCellRange == "value":
388
+ return written_range
389
+
390
+ if self.verbose:
391
+ logging.info(f"Image Written in Cell Range: {self.to_cell_range_text(*written_range)}")
392
+
393
+ return 0
394
+
395
+ def __add_chart_data_tab(self, tabname, hide = True, max_num = 99999):
396
+ """ Add a temp tab for chart data. """
397
+ i = 0
398
+ while i <= max_num:
399
+ tabname_i = (tabname + str(i))
400
+ if tabname_i not in self.ws_dict:
401
+ ws = self.add_worksheet(tabname_i, reset_loc = False,
402
+ cell_scale = None,
403
+ auto_fit = False,
404
+ zoom_perc = 100)
405
+ ws.hide()
406
+ return ws
407
+ i += 1
408
+ return -1
409
+
410
+ def _transpose_df_for_chart(self, df, y_list, x = None):
411
+ """ Transpose Dataframe for Chart Structrue. """
412
+
413
+ _df = df.copy()
414
+ _df.columns.name = None
415
+ _df.index.name = None
416
+
417
+ if x is None:
418
+ _df = _df[y_list].T
419
+ _df = _df.reset_index(drop=False)
420
+ else:
421
+ x = x if isinstance(x, list) else [x]
422
+ _df = tanspose_dataframe(_df[[*x, *y_list]], x)
423
+
424
+ _df.columns.name = None
425
+ _df.index.name = None
426
+ return _df
427
+
428
+ def __convert_to_none_tuple(self, x = None):
429
+ """ Convert None Type to Tuple of Nones. """
430
+ if x is None:
431
+ return (None, None)
432
+ return x
433
+
434
+ def __validate_input_chart_obj(self, chart_type, input_chart):
435
+ """ Validate if the given append_to_chart object is aligned with chart_type argument. """
436
+
437
+ if chart_type == "line":
438
+ return isinstance(input_chart, xlsxwriter.chart_line.ChartLine)
439
+ if chart_type in ["column", "stacked_column"]:
440
+ return isinstance(input_chart, xlsxwriter.chart_column.ChartColumn)
441
+ if chart_type == "pie":
442
+ return isinstance(input_chart, xlsxwriter.chart_pie.ChartPie)
443
+
444
+ def write_chart(self, worksheet, df, y_list, x = None, title = "",
445
+ chart_size = (30, 13), chart_type = "line",
446
+ y_axis_range = (None, None), y_num_format = None,
447
+ y2_axis = False, loc = None, retChart = False,
448
+ skipby = "row", outputData = False, retCellRange = None,
449
+ xy_axes_name = ("", ""), major_gridlines = False,
450
+ legend = "bottom", line_marker = "circle", line_type = "solid",
451
+ chart_style = None, append_to_chart = None):
452
+ """ Write line chart to Excel worksheet."""
453
+
454
+ if isinstance(chart_type, str):
455
+ if chart_type == "line":
456
+ chart_type = {'type': 'line'}
457
+ elif chart_type == "column":
458
+ chart_type = {'type': 'column'}
459
+ elif chart_type == "stacked_column":
460
+ chart_type = {'type': 'column', "subtype": "stacked"}
461
+ elif chart_type == "pie":
462
+ chart_type = {"type": "pie"}
463
+ else:
464
+ raise ValueError(" Please select one from 'line', 'column', 'stacked_column' or 'pie'. ")
465
+
466
+ start_row = loc[0] if loc else self.curr_row
467
+ start_col = loc[1] if loc else self.curr_col
468
+
469
+ y_axis_range = self.__convert_to_none_tuple(y_axis_range)
470
+
471
+ df_t = self._transpose_df_for_chart(df = df, y_list = y_list, x = x)
472
+ if x is None:
473
+ x = list(df.index.names)
474
+ if isinstance(x, str):
475
+ x = [x]
476
+ y = "index" if len(x) == 1 else tuple(["index"] + [''] * (len(x) - 1))
477
+ x_list = [x for x in df_t.columns if x != y]
478
+
479
+ data_nrows = df_t.shape[0]
480
+ data_ncols = df_t.shape[1]
481
+
482
+ raw_data_ws = self.__add_chart_data_tab("__CHRT_DATA_")
483
+
484
+ ## if y not given, then use index value as y series.
485
+ data_index_as_y = True
486
+ if y is None:
487
+ sel_df = df_t[[*x_list]]
488
+ else:
489
+ sel_df = df_t[[y, *x_list]]
490
+ sel_df = sel_df.set_index(y)
491
+ sel_df.index.name = None
492
+
493
+ ## if output chart data to current worksheet.
494
+ if outputData:
495
+ raw_data_ws = worksheet
496
+ df_range = self.write_dataframe(worksheet = raw_data_ws, df = sel_df, index = data_index_as_y, title = None, skipby=None, retCellRange="value", loc = (start_row, start_col))
497
+ chart_loc = (df_range[2] + self.gap_number, df_range[1])
498
+ else:
499
+ df_range = self.write_dataframe(worksheet = raw_data_ws, df = sel_df, index = data_index_as_y, title = None, skipby=None, retCellRange="value", loc = (0,0))
500
+ chart_loc = (start_row, start_col)
501
+
502
+ x_count = len(x_list)
503
+ nrows = df_t.shape[0]
504
+
505
+ data_row_anchor = df_range[0]
506
+ data_col_anchor = df_range[1]
507
+ row_shift = 0
508
+
509
+ category_range = [df_range[0], df_range[1] + 1,
510
+ df_range[0] + (len(x) - 1), df_range[3]]
511
+
512
+ if self.verbose:
513
+ logging.info(f"Category Cell Range: {self.to_cell_range_text(*category_range)}")
514
+
515
+ chart = self.workbook.add_chart(chart_type)
516
+ if append_to_chart is not None:
517
+ if self.__validate_input_chart_obj(chart_type = chart_type["type"], input_chart = append_to_chart):
518
+ chart = append_to_chart
519
+ else:
520
+ warnings.warn("WARNING: Can only append data to the chart that has the same chart type as your given one.")
521
+
522
+ value_start = len(x) + 1 if len(x) > 1 else len(x)
523
+ for row_shift in range(value_start, value_start + nrows, 1):
524
+
525
+ value_range = [data_row_anchor + row_shift, data_col_anchor + 1,
526
+ data_row_anchor + row_shift, data_col_anchor + x_count]
527
+
528
+ chart.add_series({
529
+ 'name': [raw_data_ws.name, (data_row_anchor + row_shift), data_col_anchor],
530
+ 'categories': [raw_data_ws.name, *category_range],
531
+ 'values': [raw_data_ws.name, *value_range],
532
+ 'marker': {'type': line_marker},
533
+ "line" : {'dash_type': line_type},
534
+ 'data_labels': {'percentage': True} if chart_type['type'] == 'pie' else None,
535
+ 'y2_axis': y2_axis,
536
+ })
537
+ if self.verbose:
538
+ logging.info("Values Cell Range: %s", self.to_cell_range_text(*value_range))
539
+
540
+ chart.set_title({'name': title})
541
+ chart.set_legend({'position': str(legend)})
542
+ chart.set_size({"height": chart_size[0] * self.default_row_height, # by row 15
543
+ "width": chart_size[1] * self.default_col_width}) # by col 8.43
544
+
545
+ chart.set_x_axis({"name": xy_axes_name[0]})
546
+
547
+ set_y_axis_dict = {"name": xy_axes_name[1],
548
+ "major_gridlines": {"visible": int(major_gridlines)},
549
+ 'min': y_axis_range[0],
550
+ 'max': y_axis_range[1],
551
+ 'num_format': y_num_format}
552
+ if y2_axis:
553
+ chart.set_y2_axis(set_y_axis_dict)
554
+ else:
555
+ chart.set_y_axis(set_y_axis_dict)
556
+
557
+ # Setting Chart Style
558
+ if chart_style:
559
+ chart.set_style(chart_style)
560
+
561
+ # Need to Return Chart before Insert it to the worksheet.
562
+ if retChart:
563
+ return chart
564
+
565
+ worksheet.insert_chart(
566
+ chart_loc[0],
567
+ chart_loc[1],
568
+ chart
569
+ )
570
+
571
+ written_range = [start_row, start_col, int(chart_loc[0] + chart_size[0]), int(chart_loc[1] + chart_size[1])]
572
+ if outputData:
573
+
574
+ written_range = [start_row, start_col,
575
+ (start_row + data_nrows + self.gap_number + chart_size[0]),
576
+ max(df_range[3], start_col + chart_size[1])]
577
+
578
+ if skipby == 'row':
579
+ if outputData:
580
+ self.curr_row = (start_row + data_nrows + self.gap_number + chart_size[0] + self.gap_number - 1)
581
+ else:
582
+ self.curr_row = (start_row + chart_size[0] + self.gap_number - 1)
583
+ if skipby == 'col':
584
+ self.curr_col = (start_col + int(chart_size[1]) + self.gap_number)
585
+
586
+ if self.verbose:
587
+ logging.info(f"Chart Written in Cell Range: {self.to_cell_range_text(*written_range)}")
588
+
589
+ if retCellRange == "text":
590
+ return self.to_cell_range_text(*written_range)
591
+ if retCellRange == "value":
592
+ return written_range
593
+
594
+ return 0
595
+
596
+ def write_combined_chart(self, worksheet, chart1, chart2,
597
+ loc = None, chart_size = (30, 13),
598
+ skipby = "row", retCellRange = None):
599
+ """ Combined two chart objects and then write to Excel worksheet. """
600
+
601
+ start_row = loc[0] if loc else self.curr_row
602
+ start_col = loc[1] if loc else self.curr_col
603
+
604
+ chart1.combine(chart2)
605
+ worksheet.insert_chart(xl_rowcol_to_cell(start_row, start_col), chart1)
606
+
607
+ written_range = [start_row, start_col, (start_row + chart_size[0]), (start_col + chart_size[1])]
608
+
609
+ if skipby == 'row':
610
+ self.curr_row = (start_row + chart_size[0] + self.gap_number - 1)
611
+ if skipby == 'col':
612
+ self.curr_col = (start_col + chart_size[1] + self.gap_number)
613
+
614
+ if self.verbose:
615
+ logging.info(f"Chart Written in Cell Range: {self.to_cell_range_text(*written_range)}")
616
+
617
+ if retCellRange == "text":
618
+ return self.to_cell_range_text(*written_range)
619
+ if retCellRange == "value":
620
+ return written_range
621
+
622
+ return 0
623
+
624
+
625
+ def write_duo_chart(self, worksheet, df,
626
+ y1_list, y2_list = None, x = None,
627
+ c1_type = "column", c2_type = "line",
628
+ y1_axis_range = (0, 1), y2_axis_range = None,
629
+ y1_num_format = None, y2_num_format = None,
630
+ y1_line_marker = "circle", y2_line_marker = "circle",
631
+ y1_line_type = "solid", y2_line_type = "solid",
632
+ loc = None, title = "", chart_size = (30, 13),
633
+ xy_axes_name = ("", ""), major_gridlines = False,
634
+ retChart = False, retCellRange = None,
635
+ skipby = "row"):
636
+ """ Write duo-chart sharing the same x axis. """
637
+
638
+ start_row = loc[0] if loc else self.curr_row
639
+ start_col = loc[1] if loc else self.curr_col
640
+
641
+ y2_axis = True
642
+ if y2_list is None:
643
+ y2_axis = False
644
+
645
+ if y2_axis_range is None:
646
+ y2_axis_range = y1_axis_range
647
+
648
+ xy_name = (xy_axes_name[0], xy_axes_name[1])
649
+ xy2_name = (xy_axes_name[0], xy_axes_name[1])
650
+ if y2_axis and len(xy_axes_name) >= 3:
651
+ xy_name = (xy_axes_name[0], xy_axes_name[1])
652
+ xy2_name = (xy_axes_name[0], xy_axes_name[2])
653
+ if y2_axis and len(xy_axes_name) < 3:
654
+ xy_name = (xy_axes_name[0], xy_axes_name[1])
655
+ xy2_name = (xy_axes_name[0], "")
656
+
657
+ chart1 = self.write_chart(df = df,
658
+ x = x,
659
+ y_list = y1_list,
660
+ worksheet = worksheet,
661
+ title = title,
662
+ chart_type = c1_type,
663
+ chart_size = chart_size,
664
+ y_axis_range = y1_axis_range,
665
+ xy_axes_name = xy_name,
666
+ major_gridlines = major_gridlines,
667
+ y_num_format = y1_num_format,
668
+ line_type = y1_line_type,
669
+ line_marker = y1_line_marker,
670
+ retChart = True)
671
+
672
+ chart2 = self.write_chart(df = df,
673
+ x = x,
674
+ y_list = y2_list,
675
+ worksheet=worksheet,
676
+ title = title,
677
+ chart_type = c2_type,
678
+ chart_size = chart_size,
679
+ y_axis_range = y2_axis_range,
680
+ y2_axis = y2_axis,
681
+ xy_axes_name = xy2_name,
682
+ major_gridlines = False,
683
+ y_num_format = y2_num_format,
684
+ line_type = y2_line_type,
685
+ line_marker = y2_line_marker,
686
+ retChart=True)
687
+
688
+ # Need to Return Chart before Insert it to the worksheet.
689
+ if retChart:
690
+ return (chart1, chart2)
691
+
692
+ cell_range = self.write_combined_chart(worksheet,
693
+ chart1, chart2,
694
+ loc = loc, chart_size = chart_size,
695
+ skipby = skipby, retCellRange = retCellRange)
696
+
697
+ return cell_range
698
+
699
+ def write_text_by_dict(self, worksheet, dict_cells):
700
+ """ Write text using Python dictionary. """
701
+ for cell in dict_cells:
702
+ list_contents = dict_cells[cell]
703
+
704
+ if ':' in cell:
705
+ text, cell_format = list_contents
706
+ worksheet.merge_range(cell, text, self.dict_cell_format[cell_format])
707
+
708
+ else:
709
+ if len(list_contents) == 1:
710
+ list_items = []
711
+
712
+ # Text formats begin with '~~~'
713
+ list_mixed_items = list_contents[0]
714
+ for item in list_mixed_items:
715
+ if item.startswith('~~~'):
716
+ cell_format = item.split('~~~')[1]
717
+ item = self.dict_cell_format[cell_format]
718
+
719
+ list_items.append(item)
720
+ else:
721
+ list_items.append(item)
722
+
723
+ worksheet.write_rich_string(cell, *list_items)
724
+
725
+ else:
726
+ text, cell_format = list_contents
727
+ worksheet.write(cell, text, self.dict_cell_format[cell_format])
728
+
729
+ def __split_line_by_format_sign(self, line):
730
+ """ To split line by specified format sign '{}'. """
731
+
732
+ ## find all format sign '{}'
733
+ format_sign = re.findall(".*?{(.*?)}.*?", line)
734
+ format_sign = format_sign if len(format_sign) != 0 else ['']
735
+ format_sign = [x.lstrip().rstrip() for x in format_sign]
736
+
737
+ ## find all cell formats
738
+ cell_format_sign = re.findall(".*?\[\[(.*?)\]\]$", line.strip().replace("\n", ""))
739
+ cell_format_sign = [x.lstrip().rstrip() for x in cell_format_sign]
740
+
741
+ ## clean up text by removing cell format express
742
+ if len(cell_format_sign) > 0:
743
+ line = line.split("[[")[0]
744
+
745
+ ## find text before the first format sign appeared.
746
+ text_bf_curly = re.findall(r"(.*?)\{", line)
747
+ text_bf_curly = [text_bf_curly[0]] if len(text_bf_curly) != 0 else ['']
748
+ text_bf_curly = [x.strip("\n") for x in text_bf_curly]
749
+
750
+ ## find text after the first format sign appeared.
751
+ text_af_curly = re.findall(r"\}\s*(.*?)(?=\s*\{|$)", line) if re.search(".*?{(.*?)}.*?", line) else [line]
752
+ text_af_curly = [x.strip("\n") for x in text_af_curly]
753
+
754
+ return text_bf_curly, format_sign, text_af_curly, cell_format_sign
755
+
756
+ def __parse_line_by_format_sign(self, worksheet, loc, line):
757
+ """ Parse text by format sign. """
758
+
759
+ text_bf_curly, format_sign, text_af_curly, cell_format_sign = self.__split_line_by_format_sign(line)
760
+
761
+ start_text = text_bf_curly[0]
762
+ # if len(start_text) > 0 or len(format_sign) >= 2:
763
+ if len(format_sign) >= 2:
764
+ # if one line has more than 2 formats specified.
765
+
766
+ res = [loc, start_text] if start_text != '' else [loc]
767
+ assert len(format_sign) == len(text_af_curly)
768
+
769
+ for fmt, s2 in zip(format_sign, text_af_curly):
770
+ res.append(self.dict_cell_format[fmt] if fmt else self.dict_cell_format[""])
771
+ res.append(s2.strip('\n')) # remove enter sign
772
+
773
+ # Write line by line
774
+ if self.verbose:
775
+ logging.info(res)
776
+
777
+ worksheet.write_rich_string(*res)
778
+
779
+ if len(format_sign) < 2:
780
+ # if one line has more than 2 formats specified.
781
+
782
+ res = [loc, start_text] if start_text != '' else [loc]
783
+ assert len(format_sign) == len(text_af_curly)
784
+
785
+ fmt = format_sign[0]
786
+ text = text_af_curly[0].strip("\n")
787
+ res.append(text)
788
+ res.append(self.dict_cell_format[fmt])
789
+
790
+ # Write line by line
791
+ if self.verbose:
792
+ logging.info(res)
793
+
794
+ worksheet.write_string(*res)
795
+
796
+ if len(cell_format_sign) > 0:
797
+ self.set_cell_format(worksheet = worksheet, cell_range = loc, cformat = cell_format_sign[0])
798
+ return 0
799
+
800
+ def write_text_content(self, worksheet, input_text = None, txt_path = None, loc = None, retCellRange = None):
801
+ """ Write text content line by line to the worksheet. """
802
+
803
+ row_anchor = loc[0] if loc else self.curr_row
804
+ col_anchor = loc[1] if loc else self.curr_col
805
+
806
+ start_row = row_anchor
807
+ start_col = col_anchor
808
+
809
+ if (input_text) and (txt_path):
810
+ raise ValueError("Please give either input_text or txt_path, not both !!!")
811
+
812
+ if (input_text is None) and (txt_path is None):
813
+ raise ValueError("Please specify either input_text or txt_path.")
814
+
815
+ if txt_path is not None:
816
+ file_notes = open(txt_path, 'r')
817
+ textlines = file_notes.readlines()
818
+
819
+ if input_text is not None:
820
+ textlines = input_text.split("\n")
821
+
822
+ for line in textlines:
823
+ # Write text line by line
824
+ loc = xl_rowcol_to_cell(row_anchor, col_anchor)
825
+
826
+ if " [>] " in line:
827
+ line_split = line.split(" [>] ")
828
+ col_shift = 0
829
+ for sub_line in line_split:
830
+ sub_line = sub_line.rstrip()
831
+ self.__parse_line_by_format_sign(worksheet = worksheet, loc = loc, line = sub_line)
832
+ col_shift += 1
833
+ loc = xl_rowcol_to_cell(row_anchor, col_anchor + col_shift)
834
+ else:
835
+ self.__parse_line_by_format_sign(worksheet = worksheet, loc = loc, line = line)
836
+
837
+ row_anchor += 1
838
+
839
+ self.curr_row = (row_anchor - 1)
840
+
841
+ end_row = row_anchor
842
+ end_col = col_anchor
843
+
844
+ written_range = [start_row, start_col, end_row, end_col]
845
+
846
+ if self.verbose:
847
+ logging.info(f"Text Written in Cell Range: {self.to_cell_range_text(*written_range)}")
848
+
849
+ if retCellRange == "text":
850
+ return self.to_cell_range_text(*written_range)
851
+ if retCellRange == "value":
852
+ return written_range
853
+
854
+ return 0
855
+
856
+ @staticmethod
857
+ def plot_boxplot(df, x, y, y_percentage = False, colored_box = True, color_grp = (10, 1), title = "", fontsize = 14, figsize = (8, 6), show_fig = True, img_path = None, transp_bg = False):
858
+ """ Plot Box Plot Chart using matplotlib. """
859
+
860
+ plt.style.use('default')
861
+
862
+ fig, ax = plt.subplots(1, 1, figsize = figsize, dpi=200)
863
+
864
+ box_plot_data = convert_to_boxplot_data(df, x, y, True)
865
+
866
+ bplot = ax.boxplot(box_plot_data.values(), patch_artist=colored_box, widths = 0.4)
867
+ ax.set_xticklabels(box_plot_data.keys(), fontsize = fontsize)
868
+ ax.set_ylabel(string_proc(y), color='black', fontsize = fontsize)
869
+ ax.set_xlabel(string_proc(x), color='black', fontsize = fontsize)
870
+ ax.tick_params(axis='both', labelsize=fontsize)
871
+ ax.grid(True, 'major', 'y', ls='--', lw=.5, c='k', alpha=.3)
872
+ ax.set_title(title, fontsize=fontsize + 4)
873
+
874
+ if colored_box:
875
+ colors = color_input_validation(color_grp, len(box_plot_data.keys()))
876
+ for patch, color in zip(bplot['boxes'], colors):
877
+ patch.set_facecolor(color)
878
+ patch.set_alpha(.7)
879
+
880
+ if y_percentage:
881
+ plt.gca().yaxis.set_major_formatter(PercentFormatter(100))
882
+ plt.tight_layout()
883
+
884
+ if show_fig:
885
+ plt.show()
886
+
887
+ if img_path:
888
+ fig.savefig(img_path, transparent = transp_bg, dpi=200)
889
+
890
+ plt.close()
891
+ return fig, ax
892
+
893
+ def write_boxplot(self, ws, df, x, y, y_percentage = False, colored_box = True, color_grp = (10, 1), title = "",
894
+ fontsize = 14, figsize = (30, 13), show_fig = False, img_path = None, transp_bg = False,
895
+ loc = None, skipby = "row", retCellRange = None):
896
+ """ Write boxplot to Worksheet. """
897
+
898
+ currTime = getCurrentDateTime()
899
+ rn = str(random.randrange(0, 1000))
900
+ rn_num = currTime + rn
901
+ tmp_image_path = f"./.tmp_image_{rn_num}.png"
902
+
903
+ ExcelMaster.plot_boxplot(df = df,
904
+ x = x, y = y,
905
+ y_percentage = y_percentage,
906
+ colored_box = colored_box,
907
+ color_grp = color_grp,
908
+ title = title,
909
+ fontsize = fontsize,
910
+ show_fig = show_fig,
911
+ transp_bg = transp_bg,
912
+ img_path = tmp_image_path)
913
+
914
+ self._resize_image(tmp_image_path, figsize, tmp_image_path)
915
+ ret_range = self.insert_image(ws, figPath=tmp_image_path, figScale=(1, 1), loc = loc, skipby = skipby, retCellRange = retCellRange)
916
+
917
+ return ret_range