Skip to content

ExcelMaster

Excel report engine. ExcelMaster wraps xlsxwriter and adds a cursor: every write advances the current row, so you stack tables, images, and charts on a sheet without computing cell coordinates. It also provides charts and conditional formatting. User guide: Excel Report Generation.

The package has no top-level exports. Import the class from its module:

from ExcelMaster.ExcelMaster import ExcelMaster

Cell Formats: ExcelFormatTool

ExcelFormatTool

ExcelFormat

Initialize excel format.

Opens the output workbook and registers the library of preset cell formats (dict_cell_format) that the writer classes ExcelWorkbook and ExcelMaster refer to by name.

Parameters:

Name Type Description Default
filepath str

Path of the .xlsx file to create. The file is opened when the object is created (through pandas.ExcelWriter with the xlsxwriter engine) and receives its content only when the workbook is closed, so a workbook that is never closed stays an empty file.

required

Attributes:

Name Type Description
engine ExcelWriter

Writer (xlsxwriter engine) through which DataFrames are written.

workbook Workbook

The underlying xlsxwriter workbook (engine.book).

basename str

File name part of filepath.

base_filepath str

Directory part of filepath including the trailing separator ("out/" for "out/report.xlsx"), or an empty string for a bare file name.

dict_cell_format dict

Maps format names to xlsxwriter Format objects. It holds 87 preset names, several of which are aliases of the same format (for example HEADER_1 and #), plus the names added with add_new_format. Examples of presets: BLUE_H4, ORANGE_H3, TABLE_HEADER, ----, NUM%.2, B, YELLOW_BG; list all of them with sorted(dict_cell_format). Every method that takes a format name looks it up here and raises KeyError for an unknown name. The presets are also set as attributes named after their long names (for example HEADER_1).

Notes

Register your own formats with add_new_format; presets cannot be overridden.

Source code in ExcelMaster/ExcelFormatTool.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
class ExcelFormat:
    '''Initialize excel format.

    Opens the output workbook and registers the library of preset cell formats (``dict_cell_format``) that the writer
    classes ``ExcelWorkbook`` and ``ExcelMaster`` refer to by name.

    Parameters
    ----------
    filepath : str
        Path of the ``.xlsx`` file to create. The file is opened when the object is created (through ``pandas.ExcelWriter``
        with the xlsxwriter engine) and receives its content only when the workbook is closed, so a workbook that is never
        closed stays an empty file.

    Attributes
    ----------
    engine : pandas.ExcelWriter
        Writer (xlsxwriter engine) through which DataFrames are written.
    workbook : xlsxwriter.workbook.Workbook
        The underlying xlsxwriter workbook (``engine.book``).
    basename : str
        File name part of ``filepath``.
    base_filepath : str
        Directory part of ``filepath`` including the trailing separator (``"out/"`` for ``"out/report.xlsx"``), or an
        empty string for a bare file name.
    dict_cell_format : dict
        Maps format names to xlsxwriter ``Format`` objects. It holds 87 preset names, several of which are aliases of the
        same format (for example ``HEADER_1`` and ``#``), plus the names added with ``add_new_format``. Examples of
        presets: ``BLUE_H4``, ``ORANGE_H3``, ``TABLE_HEADER``, ``----``, ``NUM%.2``, ``B``, ``YELLOW_BG``; list all of them
        with ``sorted(dict_cell_format)``. Every method that takes a format name looks it up here and raises ``KeyError``
        for an unknown name. The presets are also set as attributes named after their long names (for example
        ``HEADER_1``).

    Notes
    -----
    Register your own formats with ``add_new_format``; presets cannot be overridden.
    '''

    def __init__(self, filepath):
        '''Create the workbook for ``filepath`` and register the preset cell formats.

        The parameters and attributes are described in the class docstring.
        '''

        self.engine = pd.ExcelWriter(filepath, engine='xlsxwriter')
        self.workbook = self.engine.book
        self.basename = os.path.basename(filepath)
        self.base_filepath = filepath[:len(filepath) - len(self.basename)]


        ######### Basic Text-related Formatting ###############

        self.dict_cell_format = {}

        self.TEXT_NO_FORMAT = self.workbook.add_format({'font_name': 'Calibri', 'font_size': 11})
        self.dict_cell_format['TEXT_NO_FORMAT'] = self.TEXT_NO_FORMAT
        self.dict_cell_format[''] = self.TEXT_NO_FORMAT

        self.HEADER_1 = self.workbook.add_format({
            'bold': True,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 18,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_1'] = self.HEADER_1
        self.dict_cell_format['#'] = self.HEADER_1

        self.HEADER_2 = self.workbook.add_format({
            'bold': True,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 16,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_2'] = self.HEADER_2
        self.dict_cell_format['##'] = self.HEADER_2

        self.HEADER_3 = self.workbook.add_format({
            'bold': True,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 14,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_3'] = self.HEADER_3
        self.dict_cell_format['###'] = self.HEADER_3

        self.HEADER_4 = self.workbook.add_format({
            'bold': True,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 12,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_4'] = self.HEADER_4
        self.dict_cell_format['####'] = self.HEADER_4

        self.HEADER_1I = self.workbook.add_format({
            'bold': True,
            'underline': True,            
            'font_name': 'Calibri',
            'font_size': 18,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_1I'] = self.HEADER_1I
        self.dict_cell_format['#_'] = self.HEADER_1I

        self.HEADER_2I = self.workbook.add_format({
            'bold': True,
            'underline': True,            
            'font_name': 'Calibri',
            'font_size': 16,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_2I'] = self.HEADER_2I
        self.dict_cell_format['##_'] = self.HEADER_2I

        self.HEADER_3I = self.workbook.add_format({
            'bold': True,
            'underline': True,            
            'font_name': 'Calibri',
            'font_size': 14,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_3I'] = self.HEADER_3I
        self.dict_cell_format['###_'] = self.HEADER_3I

        self.HEADER_4I = self.workbook.add_format({
            'bold': True,
            'underline': True,            
            'font_name': 'Calibri',
            'font_size': 12,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['HEADER_4I'] = self.HEADER_4I
        self.dict_cell_format['####_'] = self.HEADER_4I

        self.TEXT_BOLD = self.workbook.add_format({
            'bold': True,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            'align': 'left'
        })
        self.dict_cell_format['TEXT_BOLD'] = self.TEXT_BOLD
        self.dict_cell_format['B'] = self.TEXT_BOLD
        self.dict_cell_format['**'] = self.TEXT_BOLD

        self.TEXT_ITALIC = self.workbook.add_format({
            'bold': False,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            'align': 'left',
            'italic': True
        })
        self.dict_cell_format['TEXT_ITALIC'] = self.TEXT_ITALIC
        self.dict_cell_format['I'] = self.TEXT_ITALIC
        self.dict_cell_format['*'] = self.TEXT_ITALIC

        self.TEXT_UNDERLINE = self.workbook.add_format({
            'bold': False,
            'underline': True,            
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            'align': 'left',
            'italic': False
        })
        self.dict_cell_format['TEXT_UNDERLINE'] = self.TEXT_UNDERLINE
        self.dict_cell_format['U'] = self.TEXT_UNDERLINE
        self.dict_cell_format['_'] = self.TEXT_UNDERLINE

        self.TEXT_NO_FORMAT_BORDER = self.workbook.add_format({
            'bold': False,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            'align': 'left',
            'border': 1
        })
        self.dict_cell_format['TEXT_NO_FORMAT_BORDER'] = self.TEXT_NO_FORMAT_BORDER  
        self.dict_cell_format['BORDER'] = self.TEXT_NO_FORMAT_BORDER
        self.dict_cell_format['----'] = self.TEXT_NO_FORMAT_BORDER

        self.TEXT_BORDER_CENTER = self.workbook.add_format({
            'bold': False,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            'align': 'center',
            'valign': 'vcenter',
            'border': 1
        })
        self.dict_cell_format['TEXT_BORDER_CENTER'] = self.TEXT_BORDER_CENTER  
        self.dict_cell_format['BORDER_CENTER'] = self.TEXT_BORDER_CENTER
        self.dict_cell_format['----C'] = self.TEXT_BORDER_CENTER

        self.TEXT_RED = self.workbook.add_format({
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#FF3800',
            'align': 'left'
        })
        self.dict_cell_format['TEXT_RED'] = self.TEXT_RED
        self.dict_cell_format['RED'] = self.TEXT_RED

        self.TEXT_RED_BOLD = self.workbook.add_format({
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#FF3800',
            "bold": True,
            'align': 'left'
        })
        self.dict_cell_format['TEXT_RED_BOLD'] = self.TEXT_RED_BOLD
        self.dict_cell_format['**RED'] = self.TEXT_RED_BOLD

        self.TEXT_BOLD_UNDERLINE = self.workbook.add_format({
            'bold': True,
            'underline': True,
            'font_name': 'Calibri',
            'font_size': 11,
            'align': 'left'
        })
        self.dict_cell_format['TEXT_BOLD_UNDERLINE'] = self.TEXT_BOLD_UNDERLINE
        self.dict_cell_format['BU'] = self.TEXT_BOLD_UNDERLINE
        self.dict_cell_format['**_'] = self.TEXT_BOLD_UNDERLINE

        self.TEXT_ITALIC_UNDERLINE = self.workbook.add_format({
            'bold': False,
            'underline': True,
            'italic': True,
            'font_name': 'Calibri',
            'font_size': 11,
            'align': 'left'
        })
        self.dict_cell_format['TEXT_ITALIC_UNDERLINE'] = self.TEXT_ITALIC_UNDERLINE
        self.dict_cell_format['IU'] = self.TEXT_ITALIC_UNDERLINE
        self.dict_cell_format['*_'] = self.TEXT_ITALIC_UNDERLINE

        self.TEXT_BOLD_ITALIC_UNDERLINE = self.workbook.add_format({
            'bold': True,
            'underline': True,
            'italic': True,
            'font_name': 'Calibri',
            'font_size': 11,
            'align': 'left'
        })
        self.dict_cell_format['TEXT_BOLD_ITALIC_UNDERLINE'] = self.TEXT_BOLD_ITALIC_UNDERLINE
        self.dict_cell_format['BIU'] = self.TEXT_BOLD_ITALIC_UNDERLINE
        self.dict_cell_format['***_'] = self.TEXT_BOLD_ITALIC_UNDERLINE

        self.HEADER_3_BOLD_UNDERLINE = self.workbook.add_format({
            'bold': True,
            'underline': True,
            'font_name': 'Calibri',
            'font_size': 14,
            'align': 'left'
        })
        self.dict_cell_format['HEADER_4_BOLD_UNDERLINE'] = self.HEADER_3_BOLD_UNDERLINE
        self.dict_cell_format['H3_BU'] = self.HEADER_3_BOLD_UNDERLINE
        self.dict_cell_format['####**_'] = self.HEADER_3_BOLD_UNDERLINE

        self.HEADER_4_BOLD_UNDERLINE = self.workbook.add_format({
            'bold': True,
            'underline': True,
            'font_name': 'Calibri',
            'font_size': 12,
            'align': 'left'
        })
        self.dict_cell_format['HEADER_4_BOLD_UNDERLINE'] = self.HEADER_4_BOLD_UNDERLINE
        self.dict_cell_format['H4_BU'] = self.HEADER_4_BOLD_UNDERLINE
        self.dict_cell_format['####**_'] = self.HEADER_4_BOLD_UNDERLINE

        self.TEXT_NO_FORMAT_PERCENTAGE = self.workbook.add_format({
            'bold': False,
            'underline': False,            
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            'num_format': '0.0000%',
            'align': 'left',    
            'border': 1
        })
        self.dict_cell_format['TEXT_NO_FORMAT_PERCENTAGE'] = self.TEXT_NO_FORMAT_PERCENTAGE
        self.dict_cell_format['NUM_PERCENTAGE'] = self.TEXT_NO_FORMAT_PERCENTAGE
        self.dict_cell_format['%'] = self.TEXT_NO_FORMAT_PERCENTAGE

        self.TEXT_SECTION_HEADER = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 12,
            'font_color': '#1F497D',
            'bg_color': '#C5D9F1',
            'align': 'center',
            'valign': 'vcenter',
            'text_wrap': False
        })
        self.dict_cell_format['TEXT_SECTION_HEADER'] = self.TEXT_SECTION_HEADER
        self.dict_cell_format['SECTION'] = self.TEXT_SECTION_HEADER

        self.TEXT_ITALIC_10 = self.workbook.add_format({
            'italic': True,
            'font_name': 'Calibri',
            'font_size': 10,
            'align': 'left'
        })
        self.dict_cell_format['TEXT_ITALIC_10'] = self.TEXT_ITALIC_10

        ######### Basic Text-related Formatting (END) ###############


        ######### TABLE HEADER FORMATTING  ##########################

        self.TABLE_ROW_COL_HEADER = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#1F4994',
            'align': 'left',
            'border': 1
        })
        self.dict_cell_format['ROW_COL_HEADER'] = self.TABLE_ROW_COL_HEADER

        self.TABLE_HEADER = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#000000',
            # No 'bg_color' key = no fill. xlsxwriter >= 3.2 rejects None via
            # Color._from_value(None) -> TypeError, so omit the key entirely.
            'align': 'center',
            'valign': 'vcenter',
            'text_wrap': False,
            'border': 1
        })
        self.dict_cell_format['TABLE_HEADER'] = self.TABLE_HEADER
        self.dict_cell_format['HEADER'] = self.TABLE_HEADER

        ######### TABLE HEADER FORMATTING (END)  ##########################

        ######## Colored Title Formatting #################################

        self.BG_BOLD_ORANGE_H4 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 12,
            'font_color': '#000000',
            'bg_color': '#FABF8F',
            'align': 'center',
            'border': 1
        })
        self.dict_cell_format['BG_BOLD_ORANGE_H4'] = self.BG_BOLD_ORANGE_H4
        self.dict_cell_format['ORANGE_H4'] = self.BG_BOLD_ORANGE_H4

        self.BG_BOLD_ORANGE_H3 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 14,
            'font_color': '#000000',
            'bg_color': '#FABF8F',
            'align': 'center',
            'border': 1
        })
        self.dict_cell_format['BG_BOLD_ORANGE_H3'] = self.BG_BOLD_ORANGE_H3
        self.dict_cell_format['ORANGE_H3'] = self.BG_BOLD_ORANGE_H3

        self.BG_BOLD_ORANGE_H2 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 16,
            'font_color': '#000000',
            'bg_color': '#FABF8F',
            'align': 'center',
            'border': 1
        })
        self.dict_cell_format['BG_BOLD_ORANGE_H2'] = self.BG_BOLD_ORANGE_H2
        self.dict_cell_format['ORANGE_H2'] = self.BG_BOLD_ORANGE_H2

        self.BG_BOLD_ORANGE_H1 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 18,
            'font_color': '#000000',
            'bg_color': '#FABF8F',
            'align': 'center',
            'border': 1
        })
        self.dict_cell_format['BG_BOLD_ORANGE_H1'] = self.BG_BOLD_ORANGE_H1
        self.dict_cell_format['ORANGE_H1'] = self.BG_BOLD_ORANGE_H1

        self.BG_FONT_BLUE_H4 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 12,
            'font_color': '#1F497D',
            'bg_color': '#C5D9F1',
            'align': 'center'  ,
            'border': 1
        })
        self.dict_cell_format['BG_FONT_BLUE_H4'] = self.BG_FONT_BLUE_H4
        self.dict_cell_format['BLUE_H4'] = self.BG_FONT_BLUE_H4

        self.BG_FONT_BLUE_H3 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 14,
            'font_color': '#1F497D',
            'bg_color': '#C5D9F1',
            'align': 'center'  ,
            'border': 1
        })
        self.dict_cell_format['BG_FONT_BLUE_H3'] = self.BG_FONT_BLUE_H3
        self.dict_cell_format['BLUE_H3'] = self.BG_FONT_BLUE_H3

        self.BG_FONT_BLUE_H2 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 16,
            'font_color': '#1F497D',
            'bg_color': '#C5D9F1',
            'align': 'center'  ,
            'border': 1
        })
        self.dict_cell_format['BG_FONT_BLUE_H2'] = self.BG_FONT_BLUE_H2
        self.dict_cell_format['BLUE_H2'] = self.BG_FONT_BLUE_H2

        self.BG_FONT_BLUE_H1 = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 18,
            'font_color': '#1F497D',
            'bg_color': '#C5D9F1',
            'align': 'center'  ,
            'border': 1
        })
        self.dict_cell_format['BG_FONT_BLUE_H1'] = self.BG_FONT_BLUE_H1
        self.dict_cell_format['BLUE_H1'] = self.BG_FONT_BLUE_H1

        ########## Colored Title Formatting (END) #################################

        self.SECTION_HEADER_PERCENTAGE = self.workbook.add_format({
            'bold': True,
            'font_name': 'Calibri',
            'font_size': 11,
            'font_color': '#1F497D',
            'bg_color': '#C5D9F1',
            'align': 'left',
            'valign': 'vcenter',
            'text_wrap': True,
            'border': 1,
            'num_format': '0.0000%',
        })
        self.dict_cell_format['SECTION_HEADER_PERCENTAGE'] = self.SECTION_HEADER_PERCENTAGE  
        self.dict_cell_format['SECTION%'] = self.SECTION_HEADER_PERCENTAGE

        self.TEXT_RIGHT_JUSTIFY = self.workbook.add_format({
            'font_name': 'Calibri',
            'font_size': 11,
            'align': 'right'
        })
        self.dict_cell_format['TEXT_RIGHT_JUSTIFY'] = self.TEXT_RIGHT_JUSTIFY    
        self.dict_cell_format['TEXT_RIGHT'] = self.TEXT_RIGHT_JUSTIFY

        ################# Number Formatting #################
        self.NUM_COMMA = self.workbook.add_format({'num_format': '#,##0', 'border': 1})
        self.dict_cell_format['NUM_COMMA'] = self.NUM_COMMA  
        self.dict_cell_format['NUM,'] = self.NUM_COMMA

        self.NUM_PERCENTAGE = self.workbook.add_format({'num_format': "0.0000%", 'border': 1})
        self.dict_cell_format['NUM_PERCENTAGE'] = self.NUM_PERCENTAGE  
        self.dict_cell_format['NUM%.4'] = self.NUM_PERCENTAGE

        self.NUM_PERCENTAGE = self.workbook.add_format({'num_format': "0.000%", 'border': 1})
        self.dict_cell_format['NUM_PERCENTAGE'] = self.NUM_PERCENTAGE  
        self.dict_cell_format['NUM%.3'] = self.NUM_PERCENTAGE

        self.NUM_PERCENTAGE = self.workbook.add_format({'num_format': "0.00%", 'border': 1})
        self.dict_cell_format['NUM_PERCENTAGE'] = self.NUM_PERCENTAGE  
        self.dict_cell_format['NUM%.2'] = self.NUM_PERCENTAGE

        self.NUM_PERCENTAGE = self.workbook.add_format({'num_format': "0.0%", 'border': 1})
        self.dict_cell_format['NUM_PERCENTAGE'] = self.NUM_PERCENTAGE  
        self.dict_cell_format['NUM%.1'] = self.NUM_PERCENTAGE


        ################# Highlight Formatting #################
        self.BG_LIGHT_YELLOW = self.workbook.add_format({"bg_color": "fcfc81"})
        self.dict_cell_format['BG_LIGHT_YELLOW'] = self.BG_LIGHT_YELLOW  
        self.dict_cell_format['YELLOW_BG'] = self.BG_LIGHT_YELLOW

    def add_new_format(self, format_dict, format_name):
        """Add a new user-defined format.

        Parameters
        ----------
        format_dict : dict
            xlsxwriter format properties, for example ``{"bold": True, "bg_color": "#FFE699"}``; passed to
            ``workbook.add_format``.
        format_name : str
            Name under which the format is registered in ``dict_cell_format``. Use it wherever a format name is accepted
            (``cformat``, ``titleformat``, ``headerformat``, ``valueformat``, ...).

        Returns
        -------
        int
            0 when the format was added; 1 when ``format_name`` is already registered (every preset name counts): the existing
            format is kept, nothing is overwritten, and an INFO message is logged instead of raising an exception.
        """
        if format_name not in self.dict_cell_format.keys():
            self.dict_cell_format[format_name] = self.workbook.add_format(format_dict)
            return 0
        logger.info("ERROR: Failed to add new format, the format name has already existed!")
        return 1

add_new_format

add_new_format(format_dict, format_name)

Add a new user-defined format.

Parameters:

Name Type Description Default
format_dict dict

xlsxwriter format properties, for example {"bold": True, "bg_color": "#FFE699"}; passed to workbook.add_format.

required
format_name str

Name under which the format is registered in dict_cell_format. Use it wherever a format name is accepted (cformat, titleformat, headerformat, valueformat, ...).

required

Returns:

Type Description
int

0 when the format was added; 1 when format_name is already registered (every preset name counts): the existing format is kept, nothing is overwritten, and an INFO message is logged instead of raising an exception.

Source code in ExcelMaster/ExcelFormatTool.py
def add_new_format(self, format_dict, format_name):
    """Add a new user-defined format.

    Parameters
    ----------
    format_dict : dict
        xlsxwriter format properties, for example ``{"bold": True, "bg_color": "#FFE699"}``; passed to
        ``workbook.add_format``.
    format_name : str
        Name under which the format is registered in ``dict_cell_format``. Use it wherever a format name is accepted
        (``cformat``, ``titleformat``, ``headerformat``, ``valueformat``, ...).

    Returns
    -------
    int
        0 when the format was added; 1 when ``format_name`` is already registered (every preset name counts): the existing
        format is kept, nothing is overwritten, and an INFO message is logged instead of raising an exception.
    """
    if format_name not in self.dict_cell_format.keys():
        self.dict_cell_format[format_name] = self.workbook.add_format(format_dict)
        return 0
    logger.info("ERROR: Failed to add new format, the format name has already existed!")
    return 1

Writer: ExcelMaster

ExcelMaster

ExcelWorkbook

Bases: ExcelFormat

Write anything to Excel (Workbook-level Operator).

Adds workbook-level helpers (conditional formats, color scales, data bars, cell range conversion and clean-up of temporary images) on top of ExcelFormat, which creates the workbook and the preset cell formats and provides the attributes engine, workbook, basename, base_filepath and dict_cell_format. The worksheet-level writing API is in the subclass ExcelMaster.

Parameters:

Name Type Description Default
filepath str

Path of the output .xlsx file. The file is created when the object is built and filled by close_workbook.

required
verbose bool

If True, the writing methods log the cell range they wrote through logging.info.

True

Attributes:

Name Type Description
verbose bool

The verbose argument; it can be changed at any time.

ws_dict dict

Worksheets by name, {name: xlsxwriter worksheet}. It is filled by ExcelMaster.add_worksheet and also holds the hidden chart data sheets __CHRT_DATA_<N>.

Source code in ExcelMaster/ExcelMaster.py
class ExcelWorkbook(ExcelFormat):
    """Write anything to Excel (Workbook-level Operator).

    Adds workbook-level helpers (conditional formats, color scales, data bars, cell range conversion and clean-up of
    temporary images) on top of ``ExcelFormat``, which creates the workbook and the preset cell formats and provides the
    attributes ``engine``, ``workbook``, ``basename``, ``base_filepath`` and ``dict_cell_format``. The worksheet-level
    writing API is in the subclass ``ExcelMaster``.

    Parameters
    ----------
    filepath : str
        Path of the output ``.xlsx`` file. The file is created when the object is built and filled by ``close_workbook``.
    verbose : bool, default True
        If True, the writing methods log the cell range they wrote through ``logging.info``.

    Attributes
    ----------
    verbose : bool
        The ``verbose`` argument; it can be changed at any time.
    ws_dict : dict
        Worksheets by name, ``{name: xlsxwriter worksheet}``. It is filled by ``ExcelMaster.add_worksheet`` and also holds
        the hidden chart data sheets ``__CHRT_DATA_<N>``.
    """
    def __init__(self, filepath, verbose = True):
        """Create the workbook for ``filepath`` and set ``verbose``.

        The parameters and attributes are described in the class docstring.
        """
        super().__init__(filepath)
        self.verbose = verbose
        self.ws_dict = {}

    def to_cell_range_text(self, first_row, first_col, last_row, last_col):
        """To Excel Cell Range.

        Parameters
        ----------
        first_row : int
            Zero-based index of the first row.
        first_col : int
            Zero-based index of the first column.
        last_row : int
            Zero-based index of the last row.
        last_col : int
            Zero-based index of the last column.

        Returns
        -------
        str
            The range in A1 notation, for example ``"A1:C3"`` for ``(0, 0, 2, 2)``.
        """
        return xl_range(first_row, first_col, last_row, last_col)

    def cell_range_to_loc(self, cell_range_text):
        """Convert text cell range expression to a list of values.

        Parameters
        ----------
        cell_range_text : str
            Range in A1 notation (``"A1:C3"``) or a single cell (``"B2"``).

        Returns
        -------
        list of int
            Zero-based ``[first_row, first_col, last_row, last_col]`` for a range, or ``[row, col]`` for a single cell. It is
            the inverse of ``to_cell_range_text``.
        """
        cell_range_list = cell_range_text.split(":")
        res = []
        for x in cell_range_list:
            res += list(xl_cell_to_rowcol(x))
        return res

    def colletter_to_textloc(self, row_index, col_letter):
        """Append Row Index to the given Column Letter-Formatted Index.

        Parameters
        ----------
        row_index : int
            Excel row number (one-based, as shown in Excel) appended to the column letter(s).
        col_letter : str
            A column letter such as ``"B"`` or a column letter range such as ``"B:D"``.

        Returns
        -------
        str
            A one-row range in A1 notation: ``"B6:B6"`` for ``(6, "B")`` and ``"B6:D6"`` for ``(6, "B:D")``.
        """
        col_letter = col_letter if ":" in col_letter else (col_letter + ":" + col_letter)
        output = col_letter.split(":")
        output = ":".join([col + str(row_index) for col in output])
        return output

    def set_color_scale(self, worksheet, cell_range,
                        colors = ("#F8696B", "#FFEB84", "#63BE7B")):
        """Set Color Scale.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the conditional format.
        cell_range : str or list or tuple
            Cell range in A1 notation (``"B2:B6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
        colors : tuple of str, default ("#F8696B", "#FFEB84", "#63BE7B")
            Hex colors: three give a 3-color scale (minimum, midpoint, maximum), two give a 2-color scale (minimum, maximum).

        Returns
        -------
        int
            Always 0.

        Raises
        ------
        ValueError
            If ``colors`` does not hold exactly 2 or 3 colors.
        """
        if len(colors) == 3:
            f = {
                    "type": "3_color_scale",
                    "min_color": colors[0],
                    "mid_color": colors[1],
                    "max_color": colors[2],
                }
        elif len(colors) == 2:
            f = {
                    "type": "2_color_scale",
                    "min_color": colors[0],
                    "max_color": colors[1],
                }
        else:
            raise ValueError("Please give 2 or 3 colors!")

        if isinstance(cell_range, list) or isinstance(cell_range, tuple):
            cell_range = self.to_cell_range_text(*cell_range)

        worksheet.conditional_format(cell_range, f)
        return 0

    def set_data_bar(self, worksheet, cell_range, bar_color = "#63C384"):
        """Set Data Bar in a Worksheet for a range of cells.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the conditional format.
        cell_range : str or list or tuple
            Cell range in A1 notation (``"B2:B6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
        bar_color : str, default "#63C384"
            Hex color of the bars (Excel 2010 style data bars).

        Returns
        -------
        int
            Always 0.
        """

        f = {'type': 'data_bar',
             'data_bar_2010': True,
             "bar_color": bar_color}

        if isinstance(cell_range, list) or isinstance(cell_range, tuple):
            cell_range = self.to_cell_range_text(*cell_range)

        worksheet.conditional_format(cell_range, f)
        return 0

    def set_cell_format(self, worksheet, cell_range, cformat, cell_condition = None):
        """Set format for a range of cell.

        The format is applied as an Excel conditional format (type ``no_errors``, or ``cell`` when ``cell_condition`` is
        given), not as a static cell format.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the format.
        cell_range : str or list or tuple
            Cell range in A1 notation (``"B2:B6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
        cformat : str or xlsxwriter.format.Format
            Name of a format in ``dict_cell_format`` (``KeyError`` for an unknown name) or an xlsxwriter ``Format`` object.
        cell_condition : tuple or None, default None
            ``(criteria, value)``: format only the cells that meet the condition. ``criteria`` is an xlsxwriter cell criteria
            string (``">"``, ``"<="``, ``"=="``, ``"between"``, ``"not between"``, ...; it is lower-cased) and ``value`` the
            comparison value, or a ``(minimum, maximum)`` pair for ``"between"`` and ``"not between"``. None formats all cells
            of the range that do not contain an error. A value that is not a tuple (for example a list) is silently ignored,
            as if it were None.

        Returns
        -------
        int
            Always 0.
        """

        if cell_condition and isinstance(cell_condition, tuple):
            criteria = cell_condition[0].lower()
            value = cell_condition[1]

        if isinstance(cell_range, list) or isinstance(cell_range, tuple):
            cell_range = self.to_cell_range_text(*cell_range)

        if isinstance(cformat, str):
            cformat = self.dict_cell_format[cformat]

        if cell_condition and isinstance(cell_condition, tuple):

            if (criteria == 'between') or (criteria == 'not between'):
                worksheet.conditional_format(cell_range, {"type": "cell", 
                                                          "criteria": criteria, 
                                                          "minimum": value[0],
                                                          "maximum": value[1], 
                                                          "format": cformat})

                return 0

            worksheet.conditional_format(cell_range, {"type": "cell", 
                                                      "criteria": criteria, 
                                                      "value": value, 
                                                      "format": cformat})
            return 0

        worksheet.conditional_format(cell_range, {'type': 'no_errors', "format": cformat}) 
        return 0

    def set_cell_format_rbyr(self, worksheet, start_row, condition_list, condition, ifelse_col, cformat = "YELLOW_BG"):
        """Conditionally Highlight Cells Row by Row.

        Tests every element of ``condition_list`` with ``val_input_condition`` and highlights, in the row that belongs to the
        element, the cells of the column(s) given for the true case or for the false case.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the formats.
        start_row : int
            Zero-based row of the first element of ``condition_list``. Element ``i`` is in row ``start_row + i``, that is Excel
            row ``start_row + i + 1``.
        condition_list : list
            Values to test, one per row.
        condition : tuple or str
            Condition as understood by ``val_input_condition``: ``(operator, value)`` with the operator ``>``, ``<``, ``=``,
            ``>=``, ``<=`` (or ``gt``, ``lt``, ``eq``, ``gte``, ``lte``) and a numeric value, or a string for an equality test.
        ifelse_col : tuple of str or None
            ``(column_if_true, column_if_false)``: column letters (``"B"``) or column letter ranges (``"B:D"``). A falsy entry
            (None or ``""``) means that nothing is highlighted in that case.
        cformat : str or xlsxwriter.format.Format, default "YELLOW_BG"
            Format of the highlighted cells (see ``set_cell_format``).

        Returns
        -------
        int
            Always 0.

        Notes
        -----
        An operator that ``val_input_condition`` does not know yields None, which counts as the false case.
        """

        if_col = ifelse_col[0]
        else_col = ifelse_col[1]

        i = 0
        row_concur = start_row
        while i < len(condition_list):
            target = condition_list[i]

            if val_input_condition(target, condition):
                if if_col:
                    self.set_cell_format(worksheet, 
                                       cell_range=self.colletter_to_textloc(row_concur + 1, if_col), 
                                       cformat=cformat)

            else:
                if else_col:
                    self.set_cell_format(worksheet, 
                                       cell_range=self.colletter_to_textloc(row_concur + 1, else_col), 
                                       cformat=cformat)

            i += 1
            row_concur = row_concur + 1
        return 0

    def remove_tmp_img(self, img_pattern = r".tmp_image_[0-9]+.png"):
        """Remove Temp Images.

        Deletes the temporary box plot images (``.tmp_image_<digits>.png``) that ``write_boxplot`` leaves in the current working
        directory.

        Parameters
        ----------
        img_pattern : str, default r".tmp_image_[0-9]+.png"
            Regular expression searched (``re.search``) in the names of the files found under the current directory (``"./"``,
            searched recursively).

        Returns
        -------
        int or None
            0 when at least one matching file was found and removed, None when nothing matched.

        Notes
        -----
        Files are removed by bare file name, so only matches located directly in the current working directory can be deleted;
        a match inside a sub-directory makes ``os.remove`` raise ``FileNotFoundError``. ``close_workbook`` calls this method
        after saving the workbook.
        """
        tmp_imgs = list_files(location = "./", pattern = img_pattern)
        if len(tmp_imgs) > 0:
            for f in tmp_imgs:
                os.remove(f)
            return 0
        return 

    def close_workbook(self):
        '''Finish writing the contents of the workbook and close the file.

        Fits the columns of the worksheets added with ``auto_fit=True``, saves the workbook to ``filepath`` (until then the
        file is empty) and then calls ``remove_tmp_img`` to delete the temporary box plot images from the working directory.
        '''
        for worksheet in getattr(self, "_autofit_sheets", []):
            worksheet.autofit()
        self.workbook.close()
        self.remove_tmp_img(img_pattern = r".tmp_image_[0-9]+.png")
        logging.info("All temp images have been removed.")

to_cell_range_text

to_cell_range_text(first_row, first_col, last_row, last_col)

To Excel Cell Range.

Parameters:

Name Type Description Default
first_row int

Zero-based index of the first row.

required
first_col int

Zero-based index of the first column.

required
last_row int

Zero-based index of the last row.

required
last_col int

Zero-based index of the last column.

required

Returns:

Type Description
str

The range in A1 notation, for example "A1:C3" for (0, 0, 2, 2).

Source code in ExcelMaster/ExcelMaster.py
def to_cell_range_text(self, first_row, first_col, last_row, last_col):
    """To Excel Cell Range.

    Parameters
    ----------
    first_row : int
        Zero-based index of the first row.
    first_col : int
        Zero-based index of the first column.
    last_row : int
        Zero-based index of the last row.
    last_col : int
        Zero-based index of the last column.

    Returns
    -------
    str
        The range in A1 notation, for example ``"A1:C3"`` for ``(0, 0, 2, 2)``.
    """
    return xl_range(first_row, first_col, last_row, last_col)

cell_range_to_loc

cell_range_to_loc(cell_range_text)

Convert text cell range expression to a list of values.

Parameters:

Name Type Description Default
cell_range_text str

Range in A1 notation ("A1:C3") or a single cell ("B2").

required

Returns:

Type Description
list of int

Zero-based [first_row, first_col, last_row, last_col] for a range, or [row, col] for a single cell. It is the inverse of to_cell_range_text.

Source code in ExcelMaster/ExcelMaster.py
def cell_range_to_loc(self, cell_range_text):
    """Convert text cell range expression to a list of values.

    Parameters
    ----------
    cell_range_text : str
        Range in A1 notation (``"A1:C3"``) or a single cell (``"B2"``).

    Returns
    -------
    list of int
        Zero-based ``[first_row, first_col, last_row, last_col]`` for a range, or ``[row, col]`` for a single cell. It is
        the inverse of ``to_cell_range_text``.
    """
    cell_range_list = cell_range_text.split(":")
    res = []
    for x in cell_range_list:
        res += list(xl_cell_to_rowcol(x))
    return res

colletter_to_textloc

colletter_to_textloc(row_index, col_letter)

Append Row Index to the given Column Letter-Formatted Index.

Parameters:

Name Type Description Default
row_index int

Excel row number (one-based, as shown in Excel) appended to the column letter(s).

required
col_letter str

A column letter such as "B" or a column letter range such as "B:D".

required

Returns:

Type Description
str

A one-row range in A1 notation: "B6:B6" for (6, "B") and "B6:D6" for (6, "B:D").

Source code in ExcelMaster/ExcelMaster.py
def colletter_to_textloc(self, row_index, col_letter):
    """Append Row Index to the given Column Letter-Formatted Index.

    Parameters
    ----------
    row_index : int
        Excel row number (one-based, as shown in Excel) appended to the column letter(s).
    col_letter : str
        A column letter such as ``"B"`` or a column letter range such as ``"B:D"``.

    Returns
    -------
    str
        A one-row range in A1 notation: ``"B6:B6"`` for ``(6, "B")`` and ``"B6:D6"`` for ``(6, "B:D")``.
    """
    col_letter = col_letter if ":" in col_letter else (col_letter + ":" + col_letter)
    output = col_letter.split(":")
    output = ":".join([col + str(row_index) for col in output])
    return output

set_color_scale

set_color_scale(worksheet, cell_range, colors=('#F8696B', '#FFEB84', '#63BE7B'))

Set Color Scale.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the conditional format.

required
cell_range str or list or tuple

Cell range in A1 notation ("B2:B6") or [first_row, first_col, last_row, last_col] (zero-based).

required
colors tuple of str

Hex colors: three give a 3-color scale (minimum, midpoint, maximum), two give a 2-color scale (minimum, maximum).

("#F8696B", "#FFEB84", "#63BE7B")

Returns:

Type Description
int

Always 0.

Raises:

Type Description
ValueError

If colors does not hold exactly 2 or 3 colors.

Source code in ExcelMaster/ExcelMaster.py
def set_color_scale(self, worksheet, cell_range,
                    colors = ("#F8696B", "#FFEB84", "#63BE7B")):
    """Set Color Scale.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the conditional format.
    cell_range : str or list or tuple
        Cell range in A1 notation (``"B2:B6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
    colors : tuple of str, default ("#F8696B", "#FFEB84", "#63BE7B")
        Hex colors: three give a 3-color scale (minimum, midpoint, maximum), two give a 2-color scale (minimum, maximum).

    Returns
    -------
    int
        Always 0.

    Raises
    ------
    ValueError
        If ``colors`` does not hold exactly 2 or 3 colors.
    """
    if len(colors) == 3:
        f = {
                "type": "3_color_scale",
                "min_color": colors[0],
                "mid_color": colors[1],
                "max_color": colors[2],
            }
    elif len(colors) == 2:
        f = {
                "type": "2_color_scale",
                "min_color": colors[0],
                "max_color": colors[1],
            }
    else:
        raise ValueError("Please give 2 or 3 colors!")

    if isinstance(cell_range, list) or isinstance(cell_range, tuple):
        cell_range = self.to_cell_range_text(*cell_range)

    worksheet.conditional_format(cell_range, f)
    return 0

set_data_bar

set_data_bar(worksheet, cell_range, bar_color='#63C384')

Set Data Bar in a Worksheet for a range of cells.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the conditional format.

required
cell_range str or list or tuple

Cell range in A1 notation ("B2:B6") or [first_row, first_col, last_row, last_col] (zero-based).

required
bar_color str

Hex color of the bars (Excel 2010 style data bars).

"#63C384"

Returns:

Type Description
int

Always 0.

Source code in ExcelMaster/ExcelMaster.py
def set_data_bar(self, worksheet, cell_range, bar_color = "#63C384"):
    """Set Data Bar in a Worksheet for a range of cells.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the conditional format.
    cell_range : str or list or tuple
        Cell range in A1 notation (``"B2:B6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
    bar_color : str, default "#63C384"
        Hex color of the bars (Excel 2010 style data bars).

    Returns
    -------
    int
        Always 0.
    """

    f = {'type': 'data_bar',
         'data_bar_2010': True,
         "bar_color": bar_color}

    if isinstance(cell_range, list) or isinstance(cell_range, tuple):
        cell_range = self.to_cell_range_text(*cell_range)

    worksheet.conditional_format(cell_range, f)
    return 0

set_cell_format

set_cell_format(worksheet, cell_range, cformat, cell_condition=None)

Set format for a range of cell.

The format is applied as an Excel conditional format (type no_errors, or cell when cell_condition is given), not as a static cell format.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the format.

required
cell_range str or list or tuple

Cell range in A1 notation ("B2:B6") or [first_row, first_col, last_row, last_col] (zero-based).

required
cformat str or Format

Name of a format in dict_cell_format (KeyError for an unknown name) or an xlsxwriter Format object.

required
cell_condition tuple or None

(criteria, value): format only the cells that meet the condition. criteria is an xlsxwriter cell criteria string (">", "<=", "==", "between", "not between", ...; it is lower-cased) and value the comparison value, or a (minimum, maximum) pair for "between" and "not between". None formats all cells of the range that do not contain an error. A value that is not a tuple (for example a list) is silently ignored, as if it were None.

None

Returns:

Type Description
int

Always 0.

Source code in ExcelMaster/ExcelMaster.py
def set_cell_format(self, worksheet, cell_range, cformat, cell_condition = None):
    """Set format for a range of cell.

    The format is applied as an Excel conditional format (type ``no_errors``, or ``cell`` when ``cell_condition`` is
    given), not as a static cell format.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the format.
    cell_range : str or list or tuple
        Cell range in A1 notation (``"B2:B6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
    cformat : str or xlsxwriter.format.Format
        Name of a format in ``dict_cell_format`` (``KeyError`` for an unknown name) or an xlsxwriter ``Format`` object.
    cell_condition : tuple or None, default None
        ``(criteria, value)``: format only the cells that meet the condition. ``criteria`` is an xlsxwriter cell criteria
        string (``">"``, ``"<="``, ``"=="``, ``"between"``, ``"not between"``, ...; it is lower-cased) and ``value`` the
        comparison value, or a ``(minimum, maximum)`` pair for ``"between"`` and ``"not between"``. None formats all cells
        of the range that do not contain an error. A value that is not a tuple (for example a list) is silently ignored,
        as if it were None.

    Returns
    -------
    int
        Always 0.
    """

    if cell_condition and isinstance(cell_condition, tuple):
        criteria = cell_condition[0].lower()
        value = cell_condition[1]

    if isinstance(cell_range, list) or isinstance(cell_range, tuple):
        cell_range = self.to_cell_range_text(*cell_range)

    if isinstance(cformat, str):
        cformat = self.dict_cell_format[cformat]

    if cell_condition and isinstance(cell_condition, tuple):

        if (criteria == 'between') or (criteria == 'not between'):
            worksheet.conditional_format(cell_range, {"type": "cell", 
                                                      "criteria": criteria, 
                                                      "minimum": value[0],
                                                      "maximum": value[1], 
                                                      "format": cformat})

            return 0

        worksheet.conditional_format(cell_range, {"type": "cell", 
                                                  "criteria": criteria, 
                                                  "value": value, 
                                                  "format": cformat})
        return 0

    worksheet.conditional_format(cell_range, {'type': 'no_errors', "format": cformat}) 
    return 0

set_cell_format_rbyr

set_cell_format_rbyr(worksheet, start_row, condition_list, condition, ifelse_col, cformat='YELLOW_BG')

Conditionally Highlight Cells Row by Row.

Tests every element of condition_list with val_input_condition and highlights, in the row that belongs to the element, the cells of the column(s) given for the true case or for the false case.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the formats.

required
start_row int

Zero-based row of the first element of condition_list. Element i is in row start_row + i, that is Excel row start_row + i + 1.

required
condition_list list

Values to test, one per row.

required
condition tuple or str

Condition as understood by val_input_condition: (operator, value) with the operator >, <, =, >=, <= (or gt, lt, eq, gte, lte) and a numeric value, or a string for an equality test.

required
ifelse_col tuple of str or None

(column_if_true, column_if_false): column letters ("B") or column letter ranges ("B:D"). A falsy entry (None or "") means that nothing is highlighted in that case.

required
cformat str or Format

Format of the highlighted cells (see set_cell_format).

"YELLOW_BG"

Returns:

Type Description
int

Always 0.

Notes

An operator that val_input_condition does not know yields None, which counts as the false case.

Source code in ExcelMaster/ExcelMaster.py
def set_cell_format_rbyr(self, worksheet, start_row, condition_list, condition, ifelse_col, cformat = "YELLOW_BG"):
    """Conditionally Highlight Cells Row by Row.

    Tests every element of ``condition_list`` with ``val_input_condition`` and highlights, in the row that belongs to the
    element, the cells of the column(s) given for the true case or for the false case.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the formats.
    start_row : int
        Zero-based row of the first element of ``condition_list``. Element ``i`` is in row ``start_row + i``, that is Excel
        row ``start_row + i + 1``.
    condition_list : list
        Values to test, one per row.
    condition : tuple or str
        Condition as understood by ``val_input_condition``: ``(operator, value)`` with the operator ``>``, ``<``, ``=``,
        ``>=``, ``<=`` (or ``gt``, ``lt``, ``eq``, ``gte``, ``lte``) and a numeric value, or a string for an equality test.
    ifelse_col : tuple of str or None
        ``(column_if_true, column_if_false)``: column letters (``"B"``) or column letter ranges (``"B:D"``). A falsy entry
        (None or ``""``) means that nothing is highlighted in that case.
    cformat : str or xlsxwriter.format.Format, default "YELLOW_BG"
        Format of the highlighted cells (see ``set_cell_format``).

    Returns
    -------
    int
        Always 0.

    Notes
    -----
    An operator that ``val_input_condition`` does not know yields None, which counts as the false case.
    """

    if_col = ifelse_col[0]
    else_col = ifelse_col[1]

    i = 0
    row_concur = start_row
    while i < len(condition_list):
        target = condition_list[i]

        if val_input_condition(target, condition):
            if if_col:
                self.set_cell_format(worksheet, 
                                   cell_range=self.colletter_to_textloc(row_concur + 1, if_col), 
                                   cformat=cformat)

        else:
            if else_col:
                self.set_cell_format(worksheet, 
                                   cell_range=self.colletter_to_textloc(row_concur + 1, else_col), 
                                   cformat=cformat)

        i += 1
        row_concur = row_concur + 1
    return 0

remove_tmp_img

remove_tmp_img(img_pattern='.tmp_image_[0-9]+.png')

Remove Temp Images.

Deletes the temporary box plot images (.tmp_image_<digits>.png) that write_boxplot leaves in the current working directory.

Parameters:

Name Type Description Default
img_pattern str

Regular expression searched (re.search) in the names of the files found under the current directory ("./", searched recursively).

r".tmp_image_[0-9]+.png"

Returns:

Type Description
int or None

0 when at least one matching file was found and removed, None when nothing matched.

Notes

Files are removed by bare file name, so only matches located directly in the current working directory can be deleted; a match inside a sub-directory makes os.remove raise FileNotFoundError. close_workbook calls this method after saving the workbook.

Source code in ExcelMaster/ExcelMaster.py
def remove_tmp_img(self, img_pattern = r".tmp_image_[0-9]+.png"):
    """Remove Temp Images.

    Deletes the temporary box plot images (``.tmp_image_<digits>.png``) that ``write_boxplot`` leaves in the current working
    directory.

    Parameters
    ----------
    img_pattern : str, default r".tmp_image_[0-9]+.png"
        Regular expression searched (``re.search``) in the names of the files found under the current directory (``"./"``,
        searched recursively).

    Returns
    -------
    int or None
        0 when at least one matching file was found and removed, None when nothing matched.

    Notes
    -----
    Files are removed by bare file name, so only matches located directly in the current working directory can be deleted;
    a match inside a sub-directory makes ``os.remove`` raise ``FileNotFoundError``. ``close_workbook`` calls this method
    after saving the workbook.
    """
    tmp_imgs = list_files(location = "./", pattern = img_pattern)
    if len(tmp_imgs) > 0:
        for f in tmp_imgs:
            os.remove(f)
        return 0
    return 

close_workbook

close_workbook()

Finish writing the contents of the workbook and close the file.

Fits the columns of the worksheets added with auto_fit=True, saves the workbook to filepath (until then the file is empty) and then calls remove_tmp_img to delete the temporary box plot images from the working directory.

Source code in ExcelMaster/ExcelMaster.py
def close_workbook(self):
    '''Finish writing the contents of the workbook and close the file.

    Fits the columns of the worksheets added with ``auto_fit=True``, saves the workbook to ``filepath`` (until then the
    file is empty) and then calls ``remove_tmp_img`` to delete the temporary box plot images from the working directory.
    '''
    for worksheet in getattr(self, "_autofit_sheets", []):
        worksheet.autofit()
    self.workbook.close()
    self.remove_tmp_img(img_pattern = r".tmp_image_[0-9]+.png")
    logging.info("All temp images have been removed.")

ExcelMaster

Bases: ExcelWorkbook

Write anything to Excel (Worksheet-level Operator)

Keeps a zero-based cursor (curr_row, curr_col). Every writing method starts at the cursor, or at loc when given, and then moves the cursor below (skipby='row') or to the right of (skipby='col') the block it wrote, leaving gap_number blank cells. Only that one coordinate is updated: with skipby='row' the column of the cursor keeps its value, even when loc pointed to another column. The class builds on ExcelWorkbook (conditional formats) and ExcelFormat (workbook creation and the named cell formats) and adds tables, merged titles, text, images and charts.

Parameters:

Name Type Description Default
filepath str

Path of the output .xlsx file. The file is created when the object is built and filled by close_workbook.

required
verbose bool

Required (it has no default). If True, the writing methods log the cell range they wrote through logging.info.

required
gap_number int

Spacing between consecutive blocks. The attribute gap_number is initialised to this value plus 1, so the default leaves 3 blank rows (or columns); assigning the attribute afterwards (em.gap_number = 1) sets the number of blank rows or columns exactly.

2
init_loc tuple of int

Initial cursor (row, col), zero-based. add_worksheet resets the cursor to (0, 0) unless it is called with reset_loc=False, so init_loc only matters in that case.

(0, 0)

Attributes:

Name Type Description
curr_row int

Row of the cursor (zero-based).

curr_col int

Column of the cursor (zero-based).

gap_number int

Blank rows (or columns) left after a block: exact for tables and merged cells, approximate for charts and images, whose size is measured in cells of the default size.

default_row_height int or float

Height in pixels of a row (20 at the default scale). It is used to convert chart and image sizes into cells and is overwritten by set_cell_size (hence by add_worksheet with cell_scale True or a tuple).

default_col_width int or float

Width in pixels of a column (64 at the default scale), updated like default_row_height.

max_nrows int

Number of rows of a worksheet (1048576).

max_ncols int

Number of columns of a worksheet (16384).

Source code in ExcelMaster/ExcelMaster.py
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
class ExcelMaster(ExcelWorkbook):
    """Write anything to Excel (Worksheet-level Operator)

    Keeps a zero-based cursor (``curr_row``, ``curr_col``). Every writing method starts at the cursor, or at ``loc`` when
    given, and then moves the cursor below (``skipby='row'``) or to the right of (``skipby='col'``) the block it wrote,
    leaving ``gap_number`` blank cells. Only that one coordinate is updated: with ``skipby='row'`` the column of the cursor
    keeps its value, even when ``loc`` pointed to another column. The class builds on ``ExcelWorkbook`` (conditional
    formats) and ``ExcelFormat`` (workbook creation and the named cell formats) and adds tables, merged titles, text,
    images and charts.

    Parameters
    ----------
    filepath : str
        Path of the output ``.xlsx`` file. The file is created when the object is built and filled by ``close_workbook``.
    verbose : bool
        Required (it has no default). If True, the writing methods log the cell range they wrote through
        ``logging.info``.
    gap_number : int, default 2
        Spacing between consecutive blocks. The attribute ``gap_number`` is initialised to this value plus 1, so the
        default leaves 3 blank rows (or columns); assigning the attribute afterwards (``em.gap_number = 1``) sets the
        number of blank rows or columns exactly.
    init_loc : tuple of int, default (0, 0)
        Initial cursor ``(row, col)``, zero-based. ``add_worksheet`` resets the cursor to (0, 0) unless it is called with
        ``reset_loc=False``, so ``init_loc`` only matters in that case.

    Attributes
    ----------
    curr_row : int
        Row of the cursor (zero-based).
    curr_col : int
        Column of the cursor (zero-based).
    gap_number : int
        Blank rows (or columns) left after a block: exact for tables and merged cells, approximate for charts and images,
        whose size is measured in cells of the default size.
    default_row_height : int or float
        Height in pixels of a row (20 at the default scale). It is used to convert chart and image sizes into cells and is
        overwritten by ``set_cell_size`` (hence by ``add_worksheet`` with ``cell_scale`` True or a tuple).
    default_col_width : int or float
        Width in pixels of a column (64 at the default scale), updated like ``default_row_height``.
    max_nrows : int
        Number of rows of a worksheet (1048576).
    max_ncols : int
        Number of columns of a worksheet (16384).
    """
    def __init__(self, filepath, verbose, gap_number = 2, init_loc = (0, 0)):
        """Create the workbook for ``filepath``, set ``verbose`` and initialise the cursor and the cell size defaults.

        The parameters and attributes are described in the class docstring.
        """
        super().__init__(filepath, verbose)
        self.curr_row = init_loc[0]
        self.curr_col = init_loc[1]
        self.gap_number = gap_number + 1

        self.default_row_height = 20
        self.default_col_width = 64

        self.max_nrows = 1048576
        self.max_ncols = 16384
        self._autofit_sheets = []

    def add_worksheet(self, name, hide_grid = True, reset_loc = True, cell_scale = True, auto_fit = False, zoom_perc = 100, tab_color = None):
        """Add a worksheet.

        Parameters
        ----------
        name : str
            Name of the worksheet (unique in the workbook, at most 31 characters).
        hide_grid : bool, default True
            If True, hide the gridlines on screen and in print.
        reset_loc : bool, default True
            If True, reset the cursor to (0, 0).
        cell_scale : bool or tuple of float, default True
            ``True`` writes an explicit size for every row and column of the sheet (20 px high and 64 px wide), which takes a
            few seconds and adds a few MB to the file; a tuple ``(row_scale, col_scale)`` multiplies that size (for example
            ``(1, 2)`` makes the columns twice as wide). Any other value (``False``, None) leaves the sizes of the sheet and
            ``default_row_height`` / ``default_col_width`` untouched.
        auto_fit : bool, default False
            If True, widen the columns of the sheet to fit their content. The widths are fitted by ``close_workbook`` (with
            ``worksheet.autofit()`` of xlsxwriter), once everything is written, because xlsxwriter measures the cells that
            exist at that moment. A column is only widened, never narrowed below the width the sheet already has, so with
            ``cell_scale=True`` the 64 px base width is the minimum.
        zoom_perc : int, default 100
            Zoom of the worksheet in percent (xlsxwriter accepts 10 to 400).
        tab_color : str or None, default None
            Color of the sheet tab, a hex code such as ``"#FF0000"`` or a color name such as ``"red"``. None keeps the default.

        Returns
        -------
        xlsxwriter.worksheet.Worksheet
            The new worksheet, also registered in ``ws_dict`` and in the pandas writer so that DataFrames can be written to it.

        Notes
        -----
        ``cell_scale`` True or a tuple overwrites ``default_row_height`` and ``default_col_width``, which later charts and
        images use to convert their size into cells.
        """
        ws = self.workbook.add_worksheet(name)

        if hide_grid:
            ws.hide_gridlines(2)

        if reset_loc:
            self.reset_curr_loc()

        if isinstance(cell_scale, tuple):
            self.set_cell_size(ws, cell_scale)
        if cell_scale is True:
            self.set_cell_size(ws)

        if auto_fit:
            self._autofit_sheets.append(ws)

        if tab_color:
            ws.set_tab_color(tab_color)

        ws.set_zoom(zoom_perc)
        self.ws_dict[name] = ws
        self.engine.sheets[name] = ws
        return ws

    def reset_curr_loc(self, loc = (0, 0)):
        """Reset Current Location.

        Parameters
        ----------
        loc : tuple of int, default (0, 0)
            New cursor position ``(row, col)``, zero-based.

        Returns
        -------
        int
            Always 0.
        """

        self.curr_row = loc[0]
        self.curr_col = loc[1]
        return 0

    def _reset_cell_size(self):
        """ Reset Cell Size to Default Value. """
        self.default_row_height = 20
        self.default_col_width = 64
        return 0

    def set_cell_size(self, worksheet, size_scale = (1, 1)):
        """Set Cell Size in Scale.

        Sets the pixel height of every row (all 1,048,576) and the pixel width of every column of the worksheet, and stores the
        sizes in ``default_row_height`` and ``default_col_width``, which later chart and image sizes are converted with.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet to resize.
        size_scale : tuple of float, default (1, 1)
            ``(row_scale, col_scale)`` multiplying the base cell size of 20 px high by 64 px wide. A value that is not a
            2-tuple leaves ``default_row_height`` and ``default_col_width`` as they are, and these are applied again.

        Returns
        -------
        int
            Always 0.

        Notes
        -----
        The row height is set as the default row height of the sheet. Setting the 1,048,576 rows one by one made
        xlsxwriter keep a record for each of them: about 0.5 GB of memory and 8 seconds per worksheet, and 15 MB per
        sheet in the file, so a report of twenty sheets could exhaust the memory of the machine.
        """

        if isinstance(size_scale, tuple) and len(size_scale) == 2:
            self._reset_cell_size()
            self.default_row_height = self.default_row_height * size_scale[0]
            self.default_col_width =  self.default_col_width * size_scale[1]

        # xlsxwriter works in points for rows: 1 px = 0.75 pt (what ``set_row_pixels`` converts with).
        worksheet.set_default_row(self.default_row_height * 0.75)
        worksheet.set_column_pixels(0, self.max_ncols - 1, width=self.default_col_width)
        return 0

    def get_curr_loc(self, toCell = False):
        """Get Current Location in worksheet.

        Parameters
        ----------
        toCell : bool, default False
            If True, return the cursor as an A1-style cell reference instead of a tuple.

        Returns
        -------
        tuple of int or str
            ``(curr_row, curr_col)`` (zero-based), or for example ``"C4"`` when ``toCell`` is True.
        """
        if toCell:
            return xl_rowcol_to_cell(self.curr_row, self.curr_col)
        return (self.curr_row, self.curr_col)

    def set_border_line(self, worksheet, valuerange, border_line = 1):
        """Set border line for a range of cells.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the border.
        valuerange : str or list or tuple
            Cell range in A1 notation (``"B2:D6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
        border_line : int, default 1
            xlsxwriter border style index, used for the top, bottom, left and right border of every cell of the range
            (1 thin, 2 medium, 5 thick, ...).

        Returns
        -------
        int
            Always 0.

        Notes
        -----
        The border is applied as a conditional format (through ``set_cell_format``).
        """
        border_fmt = self.workbook.add_format({'bottom': border_line, 'top': border_line, 'left': border_line, 'right': border_line})
        self.set_cell_format(worksheet = worksheet, cell_range = valuerange, cformat = border_fmt)
        return 0

    def merge_col(self, worksheet, loc = None, nrows = 1, ncols = 1, text = "", skipby = 'row', cformat = 'BLUE_H4', retCellRange = None):
        """Merge columns in a single row.

        Merges the block of ``nrows`` x ``ncols`` cells that starts at ``loc`` (one row by default) and writes ``text`` in it.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet to write on.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the top-left cell. None starts at the cursor.
        nrows : int, default 1
            Number of rows to merge.
        ncols : int, default 1
            Number of columns to merge.
        text : str, default ""
            Text written in the merged cell.
        skipby : str or None, default 'row'
            Cursor move after the write: ``'row'`` sets ``curr_row`` to ``start_row + nrows + gap_number``, ``'col'`` sets
            ``curr_col`` to ``start_col + ncols + gap_number``; any other value (for example None) leaves the cursor unchanged.
        cformat : str, default 'BLUE_H4'
            Name of a format in ``dict_cell_format``. An unknown name, and also an xlsxwriter ``Format`` object, raises
            ``KeyError``.
        retCellRange : str or None, default None
            ``"text"`` returns the written range in A1 notation (for example ``"A1:C1"``); ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

        Returns
        -------
        int or str or list of int
            0, or the written range as selected by ``retCellRange``.

        Notes
        -----
        A block of one row and one column is written as a plain cell, because xlsxwriter cannot merge a single cell.
        """

        start_row = loc[0] if loc else self.curr_row
        start_col = loc[1] if loc else self.curr_col
        written_range = [start_row, start_col, start_row + nrows - 1, start_col + ncols - 1]

        if nrows == 1 and ncols == 1:
            # xlsxwriter refuses to merge a single cell and writes nothing, so a
            # one-column table used to lose its title; write the cell directly.
            worksheet.write(start_row, start_col, text, self.dict_cell_format[cformat])
        else:
            worksheet.merge_range(*written_range, text, self.dict_cell_format[cformat])

        if self.verbose:
            logging.info(f"Merged Cells: {self.to_cell_range_text(*written_range)}")

        # Skipped by Rows/Columns
        if skipby == 'row':
            self.curr_row = (start_row + nrows + self.gap_number)
        if skipby == 'col':
            self.curr_col = (start_col + ncols + self.gap_number)

        # Return written location by Cell Text/Value Range
        if retCellRange == "text":
            return self.to_cell_range_text(*written_range)
        if retCellRange == "value":
            return written_range

        return 0

    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):
        """Write a dataframe to excel file.

        Writes the optional title row (merged across the table), the header and the values with ``DataFrame.to_excel``, then
        formats the header and the value cells.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet to write on. The table goes through the pandas writer, which finds the sheet by its name.
        df : pandas.DataFrame
            Table to write.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the top-left cell, the title row if there is one. None starts at the cursor.
        title : str or None, default None
            Title written in a merged row above the table (format ``titleformat``). None or an empty string writes no title
            row.
        index : bool, default False
            If True, write the index as the leading column(s), one per index level.
        header : bool, default True
            If True, write the column names, one header row per column level.
        skipby : str or None, default 'row'
            Cursor move after the write: ``'row'`` sets ``curr_row`` to ``start_row + nrows + gap_number`` (``nrows`` counts
            the title and header rows), ``'col'`` sets ``curr_col`` to ``start_col + ncols + gap_number`` (``ncols`` counts
            the index columns); any other value (for example None) leaves the cursor unchanged.
        titleformat : str, default "BLUE_H4"
            Name of a format in ``dict_cell_format`` for the title (a name only: a ``Format`` object raises ``KeyError``).
        headerformat : str or xlsxwriter.format.Format, default "TABLE_HEADER"
            Format of the header cells: a name in ``dict_cell_format`` or a ``Format`` object.
        valueformat : str or xlsxwriter.format.Format, default "----"
            Format of every value cell, as a name in ``dict_cell_format`` or a ``Format`` object. It applies to all columns, so
            a percentage format would also turn count columns into percentages; format single columns afterwards with
            ``set_cell_format``.
        retCellRange : str or None, default None
            ``"text"`` returns the written range in A1 notation (for example ``"A1:D6"``); ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based), title and header included; None returns 0.

        Returns
        -------
        int or str or list of int
            0, or the written range as selected by ``retCellRange``.

        Notes
        -----
        The header and value formats are applied as conditional formats (see ``set_cell_format``).
        """

        start_row = loc[0] if loc else self.curr_row
        start_col = loc[1] if loc else self.curr_col

        ncols = df.shape[1] 
        nrows = df.shape[0]

        ## Get Number of Index Columns
        index_ncols = 0
        if index:
            index_ncols = len(df.index.names)
            ncols += index_ncols

        ## Get Number of Header Rows
        header_nrows = 0
        if header:
            header_nrows = len(df.columns.names)
            nrows += header_nrows

        ## Get Number of Title Rows
        title_nrows = 0
        if title:
            title_nrows = 1
            nrows += title_nrows

        ## Get Header Range (Include Index)
        if header:
            header_start_row = (start_row + title_nrows)
            header_start_col = start_col
            header_end_row = max((start_row + header_nrows - 1), header_start_row)
            header_end_col = (start_col + ncols - 1)
            header_range = self.to_cell_range_text(header_start_row, header_start_col, 
                                                   header_end_row, header_end_col)

        ## Write DataFrame
        if title:
            self.merge_col(worksheet = worksheet, loc = loc, ncols = ncols, cformat=titleformat, 
                           text = title, skipby = None)
            df.to_excel(self.engine, sheet_name = worksheet.name, startrow = start_row + title_nrows, 
                        startcol = start_col, header = header, index = index)
        else:
            df.to_excel(self.engine, sheet_name = worksheet.name, startrow = start_row, 
                        startcol = start_col, header = header, index = index)


        ## Get Value Range (Include Index)
        value_start_row = (start_row + title_nrows + header_nrows)
        value_start_col = start_col
        value_end_row = (start_row + nrows - 1)
        value_end_col = (start_col + ncols - 1)
        value_range = [value_start_row, value_start_col, value_end_row, value_end_col]

        ## Set Format
        self.set_cell_format(worksheet = worksheet, cell_range = value_range, cformat = valueformat)
        if header:
            self.set_cell_format(worksheet = worksheet, cell_range = header_range, cformat = headerformat)

        written_range = [start_row, start_col, (start_row + nrows - 1), (start_col + ncols - 1)]

        if self.verbose:
            logging.info(f"Table Written in Cell Range: {self.to_cell_range_text(*written_range)}")

        if skipby == 'row':
            self.curr_row = (start_row + nrows + self.gap_number)
        if skipby == 'col':
            self.curr_col = (start_col + ncols + self.gap_number)

        if retCellRange == "text":
            return self.to_cell_range_text(*written_range)
        if retCellRange == "value":
            return written_range

        return 0

    def _get_image_size(self, figPath, figScale = (1, 1), retSizeInCell = True):
        """ Get Image Size in Excel Cells. """
        img = Image.open(figPath)
        w, h = img.size
        (width, height) = (img.width * figScale[0], img.height * figScale[1])
        h_in_cell = np.floor(height/self.default_row_height)
        w_in_cell = np.ceil(width/self.default_col_width)
        img.close()
        if retSizeInCell:
            return (w_in_cell, h_in_cell)
        return (width, height)

    def _resize_image(self, imgPath, resize, outPath, size_in_cell = True):
        """ Resize image. """
        # Open the image
        image = Image.open(imgPath)
        new_image = image.resize((resize[0], resize[1]))
        if size_in_cell:
            # Resize the image
            new_image = image.resize((resize[1] * self.default_col_width, resize[0] * self.default_row_height))
        # Save the resized image
        new_image.save(outPath)
        image.close()
        return 0

    def insert_image(self, worksheet, figPath, figScale = (1, 1), loc = None, skipby = 'row', retCellRange = None):
        """Insert an image to the sheet.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet to write on.
        figPath : str
            Path of the image file (for example PNG or JPEG).
        figScale : tuple of float, default (1, 1)
            ``(x_scale, y_scale)`` factors for the width and the height of the image: 1 keeps the size, 0.8 shrinks it by 20
            percent. They are factors, not pixels.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the cell that holds the top-left corner of the image. None starts at the cursor.
        skipby : str or None, default 'row'
            Cursor move after the write: ``'row'`` moves the cursor below the image, ``'col'`` to the right of it, by the size
            of the image in cells plus ``gap_number``; any other value (for example None) leaves the cursor unchanged.
        retCellRange : str or None, default None
            ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

        Returns
        -------
        int or str or list of int
            0, or the covered range as selected by ``retCellRange``.

        Notes
        -----
        The size of the image in cells is computed from the scaled pixel size and ``default_row_height`` /
        ``default_col_width`` (rows rounded down, columns rounded up), so the returned range and the cursor move are
        approximate.
        """
        start_row = loc[0] if loc else self.curr_row
        start_col = loc[1] if loc else self.curr_col

        start_cell = xl_rowcol_to_cell(start_row, start_col)
        figsize_in_cells = self._get_image_size(figPath = figPath, figScale = figScale, retSizeInCell = True)

        worksheet.insert_image(start_cell, figPath, {"x_scale": figScale[0], "y_scale": figScale[1]})

        # Skipped by Rows/Columns
        if skipby == 'row':
            self.curr_row = (start_row + int(figsize_in_cells[1]) + self.gap_number)
        if skipby == 'col':
            self.curr_col = (start_col + int(figsize_in_cells[0]) + self.gap_number)

        written_range = [start_row, start_col, 
                         start_row + int(figsize_in_cells[1]), 
                         start_col + int(figsize_in_cells[0])]

        # Return written location by Cell Text/Value Range
        if retCellRange == "text":
            return self.to_cell_range_text(*written_range)
        if retCellRange == "value":
            return written_range

        if self.verbose:
            logging.info(f"Image Written in Cell Range: {self.to_cell_range_text(*written_range)}")

        return 0

    def __add_chart_data_tab(self, tabname, hide = True, max_num = 99999):
        """ Add a temp tab for chart data. """
        i = 0
        while i <= max_num:
            tabname_i = (tabname + str(i))
            if tabname_i not in self.ws_dict:
                ws = self.add_worksheet(tabname_i, reset_loc = False, 
                                        cell_scale = None, 
                                        auto_fit = False, 
                                        zoom_perc = 100)
                ws.hide()
                return ws
            i += 1
        return -1

    def _transpose_df_for_chart(self, df, y_list, x = None):
        """ Transpose Dataframe for Chart Structrue. """

        _df = df.copy()
        _df.columns.name = None
        _df.index.name = None

        if x is None:
            _df = _df[y_list].T
            _df = _df.reset_index(drop=False)
        else:
            x = x if isinstance(x, list) else [x]
            _df = tanspose_dataframe(_df[[*x, *y_list]], x)

        _df.columns.name = None
        _df.index.name = None
        return _df

    def __convert_to_none_tuple(self, x = None):
        """ Convert None Type to Tuple of Nones. """
        if x is None:
            return (None, None)
        return x

    def __validate_input_chart_obj(self, chart_type, input_chart):
        """ Validate if the given append_to_chart object is aligned with chart_type argument. """

        if chart_type == "line":
            return isinstance(input_chart, xlsxwriter.chart_line.ChartLine)
        if chart_type in ["column", "stacked_column"]:
            return isinstance(input_chart, xlsxwriter.chart_column.ChartColumn)
        if chart_type == "pie":
            return isinstance(input_chart, xlsxwriter.chart_pie.ChartPie)

    def write_chart(self, worksheet, df, y_list, x = None, title = "", 
                    chart_size = (30, 13), chart_type = "line",
                    y_axis_range = (None, None), y_num_format = None,
                    y2_axis = False, loc = None, retChart = False, 
                    skipby = "row", outputData = False, retCellRange = None,
                    xy_axes_name = ("", ""), major_gridlines = False,
                    legend = "bottom", line_marker = "circle", line_type = "solid", 
                    chart_style = None, append_to_chart = None): 
        """Write line chart to Excel worksheet.

        Builds an xlsxwriter chart from ``df`` and inserts it at ``loc`` (or at the cursor). Despite the summary, ``chart_type``
        also gives column, stacked column and pie charts. The source data of the chart are written to a hidden worksheet named
        ``__CHRT_DATA_<N>``, or to ``worksheet`` itself with ``outputData=True``.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the chart.
        df : pandas.DataFrame
            Source data, one row per category.
        y_list : list of str
            Columns of ``df`` to plot: one series per column, named after the column.
        x : str or list of str or None, default None
            Column(s) of ``df`` that give the category labels (several columns give multi-level labels). None uses the index
            of ``df`` (all its levels).
        title : str, default ""
            Title of the chart.
        chart_size : tuple of int, default (30, 13)
            ``(rows, columns)`` measured in worksheet cells, not pixels: the chart is ``chart_size[0] * default_row_height``
            pixels high and ``chart_size[1] * default_col_width`` pixels wide.
        chart_type : str or dict, default "line"
            ``"line"``, ``"column"``, ``"stacked_column"`` or ``"pie"``; or an options dict for ``workbook.add_chart`` such as
            ``{"type": "line"}``.
        y_axis_range : tuple, default (None, None)
            ``(min, max)`` of the value axis; None entries (or None instead of the tuple) mean automatic scaling.
        y_num_format : str or None, default None
            Number format of the value axis, for example ``"0.00%"``. None sets no number format.
        y2_axis : bool, default False
            If True, plot the series on the secondary y axis; the axis settings (range, format, title, gridlines) then apply
            to that axis.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the top-left cell of the block. None starts at the cursor.
        retChart : bool, default False
            If True, return the chart object instead of inserting it: nothing is inserted and the cursor does not move. Use it
            with ``write_combined_chart`` or ``append_to_chart``.
        skipby : str or None, default "row"
            Cursor move after the insertion: ``'row'`` moves the cursor below the chart, ``'col'`` to the right of it, by the
            size of the chart (and of the data table with ``outputData``) plus ``gap_number``; any other value (for example
            None) leaves the cursor unchanged.
        outputData : bool, default False
            If True, write the table of chart data to ``worksheet`` at the insertion point and put the chart below it; the
            hidden data sheet that is created anyway then stays empty.
        retCellRange : str or None, default None
            ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0. The range is approximate.
        xy_axes_name : tuple of str, default ("", "")
            ``(x axis title, y axis title)``.
        major_gridlines : bool, default False
            If True, show the major gridlines of the value axis.
        legend : str, default "bottom"
            xlsxwriter legend position: ``"top"``, ``"bottom"``, ``"left"``, ``"right"``, ``"overlay_left"``,
            ``"overlay_right"``, or ``"none"`` to hide the legend.
        line_marker : str, default "circle"
            Marker type of every series (xlsxwriter ``marker`` type, for example ``"circle"``, ``"square"``, ``"none"``).
        line_type : str, default "solid"
            Dash type of the line of every series (xlsxwriter ``dash_type``, for example ``"solid"``, ``"dash"``,
            ``"long_dash"``).
        chart_style : int or None, default None
            xlsxwriter built-in chart style number (``chart.set_style``). None keeps the default style.
        append_to_chart : xlsxwriter.chart.Chart or None, default None
            Existing chart (obtained with ``retChart=True``) that receives the series instead of a new chart. It must be of the
            type given by ``chart_type`` (line, column or stacked column, pie); otherwise a warning is issued and a new chart
            is used.

        Returns
        -------
        xlsxwriter.chart.Chart or int or str or list of int
            The chart when ``retChart`` is True; otherwise 0, or the range covered by the chart as selected by
            ``retCellRange``.

        Raises
        ------
        ValueError
            If ``chart_type`` is a string other than ``"line"``, ``"column"``, ``"stacked_column"`` and ``"pie"``.

        Notes
        -----
        Every call adds a hidden worksheet ``__CHRT_DATA_<N>`` to the workbook (it is registered in ``ws_dict``).
        """

        if isinstance(chart_type, str):
            if chart_type == "line":
                chart_type = {'type': 'line'}
            elif chart_type == "column":
                chart_type = {'type': 'column'}
            elif chart_type == "stacked_column":
                chart_type = {'type': 'column', "subtype": "stacked"}
            elif chart_type == "pie":
                chart_type = {"type": "pie"}
            else:
                raise ValueError(" Please select one from 'line', 'column', 'stacked_column' or 'pie'. ")

        start_row = loc[0] if loc else self.curr_row
        start_col = loc[1] if loc else self.curr_col

        y_axis_range = self.__convert_to_none_tuple(y_axis_range)

        df_t = self._transpose_df_for_chart(df = df, y_list = y_list, x = x)
        if x is None:
            x = list(df.index.names)
        if isinstance(x, str):
            x = [x]
        y = "index" if len(x) == 1 else tuple(["index"] + [''] * (len(x) - 1))
        x_list = [x for x in df_t.columns if x != y]

        data_nrows = df_t.shape[0]
        data_ncols = df_t.shape[1]

        raw_data_ws = self.__add_chart_data_tab("__CHRT_DATA_")

        ## if y not given, then use index value as y series.
        data_index_as_y = True
        if y is None:
            sel_df = df_t[[*x_list]]
        else:
            sel_df = df_t[[y, *x_list]]
            sel_df = sel_df.set_index(y)
            sel_df.index.name = None

        ## if output chart data to current worksheet.
        if outputData:
            raw_data_ws = worksheet
            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))
            chart_loc = (df_range[2] + self.gap_number, df_range[1])
        else:
            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))
            chart_loc = (start_row, start_col)

        x_count = len(x_list)
        nrows = df_t.shape[0]

        data_row_anchor = df_range[0]
        data_col_anchor = df_range[1]
        row_shift = 0

        category_range = [df_range[0], df_range[1] + 1, 
                          df_range[0] + (len(x) - 1), df_range[3]]

        if self.verbose:
            logging.info(f"Category Cell Range: {self.to_cell_range_text(*category_range)}")

        chart = self.workbook.add_chart(chart_type)
        if append_to_chart is not None:
            if self.__validate_input_chart_obj(chart_type = chart_type["type"], input_chart = append_to_chart):
                chart = append_to_chart
            else:
                warnings.warn("WARNING: Can only append data to the chart that has the same chart type as your given one.")

        value_start = len(x) + 1 if len(x) > 1 else len(x)
        for row_shift in range(value_start, value_start + nrows, 1):

            value_range = [data_row_anchor + row_shift, data_col_anchor + 1, 
                           data_row_anchor + row_shift, data_col_anchor + x_count]

            chart.add_series({
                'name':        [raw_data_ws.name, (data_row_anchor + row_shift), data_col_anchor],
                'categories':  [raw_data_ws.name, *category_range],
                'values':      [raw_data_ws.name, *value_range],
                'marker':      {'type': line_marker},
                "line" :       {'dash_type': line_type},
                'data_labels': {'percentage': True} if chart_type['type'] == 'pie' else None,
                'y2_axis':     y2_axis,
            })
            if self.verbose:
                logging.info("Values Cell Range: %s", self.to_cell_range_text(*value_range))

        chart.set_title({'name': title})
        chart.set_legend({'position': str(legend)})
        chart.set_size({"height": chart_size[0] * self.default_row_height, # by row 15
                        "width": chart_size[1] * self.default_col_width}) # by col 8.43

        chart.set_x_axis({"name": xy_axes_name[0]})

        set_y_axis_dict = {"name": xy_axes_name[1], 
                           "major_gridlines": {"visible": int(major_gridlines)},
                           'min': y_axis_range[0], 
                           'max': y_axis_range[1],
                           'num_format': y_num_format}
        if y2_axis:
            chart.set_y2_axis(set_y_axis_dict)
        else:
            chart.set_y_axis(set_y_axis_dict)

        # Setting Chart Style
        if chart_style:
            chart.set_style(chart_style)

        # Need to Return Chart before Insert it to the worksheet.
        if retChart:
            return chart

        worksheet.insert_chart(
            chart_loc[0], 
            chart_loc[1], 
            chart
        )

        written_range = [start_row, start_col, int(chart_loc[0] + chart_size[0]), int(chart_loc[1] + chart_size[1])]
        if outputData:

            written_range = [start_row, start_col, 
                             (start_row + data_nrows + self.gap_number + chart_size[0]),
                             max(df_range[3], start_col + chart_size[1])]

        if skipby == 'row':
            if outputData:
                self.curr_row = (start_row + data_nrows + self.gap_number + chart_size[0] + self.gap_number - 1)
            else:
                self.curr_row = (start_row + chart_size[0] + self.gap_number - 1)
        if skipby == 'col':
            self.curr_col = (start_col + int(chart_size[1]) + self.gap_number)

        if self.verbose:
            logging.info(f"Chart Written in Cell Range: {self.to_cell_range_text(*written_range)}")

        if retCellRange == "text":
            return self.to_cell_range_text(*written_range)
        if retCellRange == "value":
            return written_range

        return 0

    def write_combined_chart(self, worksheet, chart1, chart2, 
                             loc = None, chart_size = (30, 13), 
                             skipby = "row", retCellRange = None):
        """Combined two chart objects and then write to Excel worksheet.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the combined chart.
        chart1 : xlsxwriter.chart.Chart
            Primary chart, obtained with ``retChart=True``. ``chart2`` is combined into it and it is the object inserted, so
            its title, size and primary axis apply.
        chart2 : xlsxwriter.chart.Chart or None
            Secondary chart, obtained with ``retChart=True``, combined into ``chart1`` (xlsxwriter supports only some
            combinations, for example a column chart with a line chart). None inserts ``chart1`` alone.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the top-left cell. None starts at the cursor.
        chart_size : tuple of int, default (30, 13)
            ``(rows, columns)`` in cells. It is used only to compute the returned range and the cursor move; it does not
            resize the chart, whose size was set when the charts were created.
        skipby : str or None, default "row"
            Cursor move after the insertion: ``'row'`` moves the cursor below the chart, ``'col'`` to the right of it, by
            ``chart_size`` plus ``gap_number``; any other value (for example None) leaves the cursor unchanged.
        retCellRange : str or None, default None
            ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

        Returns
        -------
        int or str or list of int
            0, or the covered range as selected by ``retCellRange``.
        """

        start_row = loc[0] if loc else self.curr_row
        start_col = loc[1] if loc else self.curr_col

        if chart2 is not None:
            chart1.combine(chart2)
        worksheet.insert_chart(xl_rowcol_to_cell(start_row, start_col), chart1)

        written_range = [start_row, start_col, (start_row + chart_size[0]), (start_col + chart_size[1])]

        if skipby == 'row':
            self.curr_row = (start_row + chart_size[0] + self.gap_number - 1)
        if skipby == 'col':
            self.curr_col = (start_col + chart_size[1] + self.gap_number)

        if self.verbose:
            logging.info(f"Chart Written in Cell Range: {self.to_cell_range_text(*written_range)}")

        if retCellRange == "text":
            return self.to_cell_range_text(*written_range)
        if retCellRange == "value":
            return written_range

        return 0


    def write_duo_chart(self, worksheet, df,
                        y1_list, y2_list = None, x = None,
                        c1_type = "column", c2_type = "line", 
                        y1_axis_range = (0, 1), y2_axis_range = None,
                        y1_num_format = None, y2_num_format = None,
                        y1_line_marker = "circle", y2_line_marker = "circle",
                        y1_line_type = "solid", y2_line_type = "solid",
                        loc = None, title = "", chart_size = (30, 13),
                        xy_axes_name = ("", ""), major_gridlines = False,
                        retChart = False, retCellRange = None,
                        skipby = "row"):
        """Write duo-chart sharing the same x axis.

        Builds two charts from ``df`` and ``x`` with ``write_chart`` (the first from ``y1_list`` on the primary y axis, the
        second from ``y2_list`` on the secondary y axis), combines them and inserts the result. Typical use: columns for counts
        on the left axis and a line for a rate on the right axis. Without ``y2_list`` only the first chart is built and
        inserted.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet that receives the chart.
        df : pandas.DataFrame
            Source data, one row per category.
        y1_list : list of str
            Columns drawn on the primary y axis, in the chart type ``c1_type``.
        y2_list : list of str, default None
            Columns drawn on the secondary y axis, in the chart type ``c2_type``. With the default None there is no second
            chart and no secondary axis: the first chart is inserted alone.
        x : str or list of str or None, default None
            Column(s) that give the category labels, as in ``write_chart``. None uses the index of ``df``.
        c1_type : str or dict, default "column"
            Type of the first chart: ``"line"``, ``"column"``, ``"stacked_column"``, ``"pie"`` or an options dict for
            ``workbook.add_chart``.
        c2_type : str or dict, default "line"
            Type of the second chart, as ``c1_type``.
        y1_axis_range : tuple or None, default (0, 1)
            ``(min, max)`` of the primary y axis. The default fixes the axis to 0 - 1, which suits rates but clips counts; pass
            None (or ``(None, None)``) for automatic scaling.
        y2_axis_range : tuple or None, default None
            ``(min, max)`` of the secondary y axis. None uses ``y1_axis_range``.
        y1_num_format : str or None, default None
            Number format of the primary y axis, for example ``"0.00%"``.
        y2_num_format : str or None, default None
            Number format of the secondary y axis.
        y1_line_marker : str, default "circle"
            Marker type of the series of the first chart.
        y2_line_marker : str, default "circle"
            Marker type of the series of the second chart.
        y1_line_type : str, default "solid"
            Dash type of the lines of the first chart.
        y2_line_type : str, default "solid"
            Dash type of the lines of the second chart.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the top-left cell. None starts at the cursor.
        title : str, default ""
            Title of the chart (the combined chart shows the title of the first one).
        chart_size : tuple of int, default (30, 13)
            ``(rows, columns)`` measured in worksheet cells, not pixels.
        xy_axes_name : tuple of str, default ("", "")
            ``(x axis title, primary y title)`` or ``(x axis title, primary y title, secondary y title)``; with two entries the
            secondary y axis has no title.
        major_gridlines : bool, default False
            If True, show the major gridlines of the primary y axis; the secondary axis never shows gridlines.
        retChart : bool, default False
            If True, return the tuple ``(chart1, chart2)`` instead of inserting anything (the cursor does not move).
        retCellRange : str or None, default None
            ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.
        skipby : str or None, default "row"
            Cursor move after the insertion: ``'row'`` moves the cursor below the chart, ``'col'`` to the right of it; any other
            value (for example None) leaves the cursor unchanged.

        Returns
        -------
        tuple of xlsxwriter.chart.Chart or int or str or list of int
            The tuple ``(chart1, chart2)`` when ``retChart`` is True (``chart2`` is None when ``y2_list`` is None); otherwise
            the result of ``write_combined_chart``: 0, or the covered range as selected by ``retCellRange``.

        Notes
        -----
        One hidden worksheet ``__CHRT_DATA_<N>`` per chart is added to the workbook.
        """

        start_row = loc[0] if loc else self.curr_row
        start_col = loc[1] if loc else self.curr_col

        y2_axis = True
        if y2_list is None:
            y2_axis = False

        if y2_axis_range is None:
            y2_axis_range = y1_axis_range

        xy_name = (xy_axes_name[0], xy_axes_name[1])
        xy2_name = (xy_axes_name[0], xy_axes_name[1])
        if y2_axis and len(xy_axes_name) >= 3:
            xy_name = (xy_axes_name[0], xy_axes_name[1])
            xy2_name = (xy_axes_name[0], xy_axes_name[2])
        if y2_axis and len(xy_axes_name) < 3:
            xy_name = (xy_axes_name[0], xy_axes_name[1])
            xy2_name = (xy_axes_name[0], "")

        chart1 = self.write_chart(df = df, 
                                  x = x, 
                                  y_list = y1_list, 
                                  worksheet = worksheet, 
                                  title = title,
                                  chart_type = c1_type, 
                                  chart_size = chart_size,
                                  y_axis_range = y1_axis_range,
                                  xy_axes_name = xy_name,
                                  major_gridlines = major_gridlines,
                                  y_num_format = y1_num_format,
                                  line_type = y1_line_type,
                                  line_marker = y1_line_marker,
                                  retChart = True)

        chart2 = None
        if y2_axis:
            chart2 = self.write_chart(df = df, 
                                      x = x,
                                      y_list = y2_list, 
                                      worksheet=worksheet, 
                                      title = title, 
                                      chart_type = c2_type, 
                                      chart_size = chart_size,
                                      y_axis_range = y2_axis_range,
                                      y2_axis = y2_axis,
                                      xy_axes_name = xy2_name,
                                      major_gridlines = False,
                                      y_num_format = y2_num_format,
                                      line_type = y2_line_type,
                                      line_marker = y2_line_marker,
                                      retChart=True)

        # Need to Return Chart before Insert it to the worksheet.
        if retChart:
            return (chart1, chart2)

        cell_range = self.write_combined_chart(worksheet, 
                                  chart1, chart2, 
                                  loc = loc, chart_size = chart_size, 
                                  skipby = skipby, retCellRange = retCellRange)

        return cell_range

    def write_text_by_dict(self, worksheet, dict_cells):
        """Write text using Python dictionary.

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet to write on.
        dict_cells : dict
            Maps a cell (``"A1"``) or a range to merge (``"M1:O2"``) to its content; formats are names in
            ``dict_cell_format``:

            * range key with ``[text, format_name]``: merge the range and write ``text`` in it;
            * cell key with ``[text, format_name]``: write ``text`` with the format;
            * cell key with ``[[item, item, ...]]`` (a one-element list that holds a list): write a rich string. An item that
              starts with ``"~~~"`` is a format name (``"~~~B"``) that applies to the text items that follow; the other items
              are text.

        Returns
        -------
        None
            Nothing is returned. The cursor is not used or changed.
        """
        for cell in dict_cells:
            list_contents = dict_cells[cell]

            if ':' in cell:
                text, cell_format = list_contents
                worksheet.merge_range(cell, text, self.dict_cell_format[cell_format])

            else:
                if len(list_contents) == 1:
                    list_items = []

                    # Text formats begin with '~~~'
                    list_mixed_items = list_contents[0]
                    for item in list_mixed_items:
                        if item.startswith('~~~'):
                            cell_format = item.split('~~~')[1]
                            item = self.dict_cell_format[cell_format]

                            list_items.append(item)
                        else:
                            list_items.append(item)

                    worksheet.write_rich_string(cell, *list_items)

                else:
                    text, cell_format = list_contents
                    worksheet.write(cell, text, self.dict_cell_format[cell_format])

    def __split_line_by_format_sign(self, line):
        """ To split line by specified format sign '{}'. """

        ## find all format sign '{}'
        format_sign = re.findall(".*?{(.*?)}.*?", line)
        format_sign = format_sign if len(format_sign) != 0 else ['']
        format_sign = [x.lstrip().rstrip() for x in format_sign]

        ## find all cell formats
        cell_format_sign = re.findall(".*?\[\[(.*?)\]\]$", line.strip().replace("\n", ""))
        cell_format_sign = [x.lstrip().rstrip() for x in cell_format_sign]

        ## clean up text by removing cell format express
        if len(cell_format_sign) > 0:
            line = line.split("[[")[0]

        ## find text before the first format sign appeared.
        text_bf_curly = re.findall(r"(.*?)\{", line)
        text_bf_curly = [text_bf_curly[0]] if len(text_bf_curly) != 0 else ['']
        text_bf_curly = [x.strip("\n") for x in text_bf_curly]

        ## find text after the first format sign appeared.
        text_af_curly = re.findall(r"\}\s*(.*?)(?=\s*\{|$)", line) if re.search(".*?{(.*?)}.*?", line) else [line]
        text_af_curly = [x.strip("\n") for x in text_af_curly]

        return text_bf_curly, format_sign, text_af_curly, cell_format_sign

    def __parse_line_by_format_sign(self, worksheet, loc, line):
        """ Parse text by format sign. """

        text_bf_curly, format_sign, text_af_curly, cell_format_sign = self.__split_line_by_format_sign(line)

        start_text = text_bf_curly[0]
        # if len(start_text) > 0 or len(format_sign) >= 2:
        if len(format_sign) >= 2:
            # if one line has more than 2 formats specified.

            res = [loc, start_text] if start_text != '' else [loc]
            assert len(format_sign) == len(text_af_curly)

            for fmt, s2 in zip(format_sign, text_af_curly):
                res.append(self.dict_cell_format[fmt] if fmt else self.dict_cell_format[""])
                res.append(s2.strip('\n')) # remove enter sign

            # Write line by line
            if self.verbose:
                logging.info(res)

            worksheet.write_rich_string(*res)

        if len(format_sign) < 2:
            # if one line has more than 2 formats specified.

            res = [loc, start_text] if start_text != '' else [loc]
            assert len(format_sign) == len(text_af_curly)

            fmt = format_sign[0]
            text = text_af_curly[0].strip("\n")
            res.append(text)
            res.append(self.dict_cell_format[fmt])

            # Write line by line
            if self.verbose:
                logging.info(res)

            worksheet.write_string(*res)

        if len(cell_format_sign) > 0:
            self.set_cell_format(worksheet = worksheet, cell_range = loc, cformat = cell_format_sign[0])
        return 0

    def write_text_content(self, worksheet, input_text = None, txt_path = None, loc = None, retCellRange = None):
        r"""Write text content line by line to the worksheet.

        Every line becomes one row, starting at ``loc`` (or the cursor), in a single column. Inline markup in a line:

        * ``{FORMAT_NAME}`` styles the text that follows it with a format of ``dict_cell_format`` (``{B}``, ``{I}``,
          ``{HEADER_2}``, ``{BLUE_H2}``, ...). Several signs in one line give a rich string with mixed formats (text before
          the first sign is allowed then). A line with exactly one sign must start with it, otherwise ``TypeError``.
        * ``" [>] "`` (with the blanks) splits a line into cells that are written side by side in the same row.
        * a trailing ``[[FORMAT_NAME]]`` formats the whole cell (as a conditional format).

        Parameters
        ----------
        worksheet : xlsxwriter.worksheet.Worksheet
            Worksheet to write on.
        input_text : str or None, default None
            Text to write, lines separated by ``"\n"``.
        txt_path : str or None, default None
            Path of a text file whose lines are written. Give exactly one of ``input_text`` and ``txt_path``.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the first line. None starts at the cursor.
        retCellRange : str or None, default None
            ``"text"`` returns the range in A1 notation; ``"value"`` returns ``[first_row, first_col, last_row, last_col]``
            (zero-based); None returns 0. The range covers only the first column, and its last row is one past the last line.

        Returns
        -------
        int or str or list of int
            0, or the range as selected by ``retCellRange``.

        Raises
        ------
        ValueError
            If both or neither of ``input_text`` and ``txt_path`` are given.

        Notes
        -----
        The cursor is set to the last row written (``curr_row = start_row + number_of_lines - 1``) and ``curr_col`` is not
        changed, even when ``loc`` is given. End ``input_text`` with ``"\n"`` so that the next block starts below the text;
        otherwise it overwrites the last line. The file given by ``txt_path`` is opened and not closed explicitly.
        """

        row_anchor = loc[0] if loc else self.curr_row
        col_anchor = loc[1] if loc else self.curr_col

        start_row = row_anchor
        start_col = col_anchor

        if (input_text) and (txt_path):
            raise ValueError("Please give either input_text or txt_path, not both !!!")

        if (input_text is None) and (txt_path is None):
            raise ValueError("Please specify either input_text or txt_path.")

        if txt_path is not None:
            file_notes = open(txt_path, 'r')
            textlines = file_notes.readlines()

        if input_text is not None:
            textlines = input_text.split("\n")

        for line in textlines:
            # Write text line by line
            loc = xl_rowcol_to_cell(row_anchor, col_anchor)

            if " [>] " in line:
                line_split = line.split(" [>] ")
                col_shift = 0
                for sub_line in line_split:
                    sub_line = sub_line.rstrip()
                    self.__parse_line_by_format_sign(worksheet = worksheet, loc = loc, line = sub_line)
                    col_shift += 1
                    loc = xl_rowcol_to_cell(row_anchor, col_anchor + col_shift)
            else:
                self.__parse_line_by_format_sign(worksheet = worksheet, loc = loc, line = line)

            row_anchor += 1

        self.curr_row = (row_anchor - 1)

        end_row = row_anchor
        end_col = col_anchor

        written_range = [start_row, start_col, end_row, end_col]

        if self.verbose:
            logging.info(f"Text Written in Cell Range: {self.to_cell_range_text(*written_range)}")

        if retCellRange == "text":
            return self.to_cell_range_text(*written_range)
        if retCellRange == "value":
            return written_range

        return 0

    @staticmethod
    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):
        """Plot Box Plot Chart using matplotlib.

        Draws one box per distinct value of ``x`` (in ascending order) from the values of ``y``. Both axis labels are
        reformatted with ``string_proc`` (``"credit_score"`` becomes ``"Credit Score"``). The figure is closed before the
        function returns.

        Parameters
        ----------
        df : pandas.DataFrame
            Source data.
        x : str
            Column that defines the groups, one box per distinct value.
        y : str
            Numeric column whose distribution is drawn in each box.
        y_percentage : bool, default False
            If True, multiply the values of ``y`` by 100 and format the y axis as percent (``PercentFormatter(100)``).
        colored_box : bool, default True
            If True, fill the boxes with the colors of ``color_grp`` (alpha 0.7); otherwise draw unfilled boxes.
        color_grp : tuple or str or list of str, default (10, 1)
            Box colors, as accepted by ``color_input_validation``: a ``(start_num, step)`` tuple that picks XKCD named colors, one
            color code for all boxes, or a list with one color code per box. Used only when ``colored_box`` is True.
        title : str, default ""
            Title of the plot.
        fontsize : int, default 14
            Font size of the axis labels and ticks (the title is ``fontsize + 4``).
        figsize : tuple of float, default (8, 6)
            Figure size ``(width, height)`` in inches (the figure is created with 200 dpi).
        show_fig : bool, default True
            If True, display the figure with ``plt.show()``.
        img_path : str or None, default None
            If given, save the figure to this path (200 dpi).
        transp_bg : bool, default False
            If True, save the image with a transparent background.

        Returns
        -------
        tuple
            ``(fig, ax)``: the matplotlib ``Figure`` and ``Axes``.

        Notes
        -----
        ``y_percentage=True`` multiplies the values of ``y`` by 100 and labels the axis as percent, so it is meant for rates
        between 0 and 1. With ``y_percentage=False`` the values are drawn as they are.
        """

        plt.style.use('default')

        fig, ax = plt.subplots(1, 1, figsize = figsize, dpi=200)

        box_plot_data = convert_to_boxplot_data(df, x, y, y_percentage)

        bplot = ax.boxplot(box_plot_data.values(), patch_artist=colored_box, widths = 0.4)
        ax.set_xticklabels(box_plot_data.keys(), fontsize = fontsize)
        ax.set_ylabel(string_proc(y), color='black', fontsize = fontsize)
        ax.set_xlabel(string_proc(x), color='black', fontsize = fontsize)
        ax.tick_params(axis='both', labelsize=fontsize)
        ax.grid(True, 'major', 'y', ls='--', lw=.5, c='k', alpha=.3)
        ax.set_title(title, fontsize=fontsize + 4)

        if colored_box:
            colors = color_input_validation(color_grp, len(box_plot_data.keys()))
            for patch, color in zip(bplot['boxes'], colors):
                patch.set_facecolor(color)
                patch.set_alpha(.7)

        if y_percentage:
            plt.gca().yaxis.set_major_formatter(PercentFormatter(100)) 
        plt.tight_layout()

        if show_fig:
            plt.show()

        if img_path:
            fig.savefig(img_path, transparent = transp_bg, dpi=200)

        plt.close()
        return fig, ax

    def write_boxplot(self, ws, df, x, y, y_percentage = False, colored_box = True, color_grp = (10, 1), title = "", 
                      fontsize = 14, figsize = (30, 13), show_fig = False, img_path = None, transp_bg = False,
                      loc = None, skipby = "row", retCellRange = None):
        """Write boxplot to Worksheet.

        Draws the box plot with ``plot_boxplot`` into a temporary PNG file (``./.tmp_image_<date time><random number>.png`` in
        the working directory), resizes the file to ``figsize`` worksheet cells and inserts it with ``insert_image``.

        Parameters
        ----------
        ws : xlsxwriter.worksheet.Worksheet
            Worksheet to write on (note the name: ``ws``, not ``worksheet``).
        df : pandas.DataFrame
            Source data.
        x : str
            Column that defines the groups, one box per distinct value.
        y : str
            Numeric column whose distribution is drawn in each box.
        y_percentage : bool, default False
            If True, multiply the values by 100 and format the y axis as percent (see ``plot_boxplot``).
        colored_box : bool, default True
            If True, fill the boxes with the colors of ``color_grp``.
        color_grp : tuple or str or list of str, default (10, 1)
            Box colors, as accepted by ``color_input_validation``.
        title : str, default ""
            Title of the plot.
        fontsize : int, default 14
            Font size of the axis labels and ticks.
        figsize : tuple of int, default (30, 13)
            Size of the inserted image as ``(rows, columns)`` of worksheet cells, not inches (unlike ``plot_boxplot``): the
            image file is resized to ``columns * default_col_width`` by ``rows * default_row_height`` pixels. It is not passed to
            the figure itself.
        show_fig : bool, default False
            If True, display the figure with ``plt.show()``.
        img_path : str or None, default None
            Not used: the image is always saved to the temporary file.
        transp_bg : bool, default False
            If True, save the image with a transparent background.
        loc : tuple of int or None, default None
            ``(row, col)`` (zero-based) of the top-left cell of the image. None starts at the cursor.
        skipby : str or None, default "row"
            Cursor move after the insertion, as in ``insert_image``.
        retCellRange : str or None, default None
            ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
            ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

        Returns
        -------
        int or str or list of int
            The result of ``insert_image``: 0, or the covered range as selected by ``retCellRange``.

        Notes
        -----
        The temporary image stays in the working directory until ``close_workbook`` (or ``remove_tmp_img``) deletes it.
        """

        currTime = getCurrentDateTime()
        rn = str(random.randrange(0, 1000))
        rn_num = currTime + rn
        tmp_image_path = f"./.tmp_image_{rn_num}.png"

        ExcelMaster.plot_boxplot(df = df, 
                                 x = x, y = y, 
                                 y_percentage = y_percentage, 
                                 colored_box = colored_box, 
                                 color_grp = color_grp, 
                                 title = title, 
                                 fontsize = fontsize, 
                                 show_fig = show_fig, 
                                 transp_bg = transp_bg,
                                 img_path = tmp_image_path)

        self._resize_image(tmp_image_path, figsize, tmp_image_path)
        ret_range = self.insert_image(ws, figPath=tmp_image_path, figScale=(1, 1), loc = loc, skipby = skipby, retCellRange = retCellRange)

        return ret_range

add_worksheet

add_worksheet(name, hide_grid=True, reset_loc=True, cell_scale=True, auto_fit=False, zoom_perc=100, tab_color=None)

Add a worksheet.

Parameters:

Name Type Description Default
name str

Name of the worksheet (unique in the workbook, at most 31 characters).

required
hide_grid bool

If True, hide the gridlines on screen and in print.

True
reset_loc bool

If True, reset the cursor to (0, 0).

True
cell_scale bool or tuple of float

True writes an explicit size for every row and column of the sheet (20 px high and 64 px wide), which takes a few seconds and adds a few MB to the file; a tuple (row_scale, col_scale) multiplies that size (for example (1, 2) makes the columns twice as wide). Any other value (False, None) leaves the sizes of the sheet and default_row_height / default_col_width untouched.

True
auto_fit bool

If True, widen the columns of the sheet to fit their content. The widths are fitted by close_workbook (with worksheet.autofit() of xlsxwriter), once everything is written, because xlsxwriter measures the cells that exist at that moment. A column is only widened, never narrowed below the width the sheet already has, so with cell_scale=True the 64 px base width is the minimum.

False
zoom_perc int

Zoom of the worksheet in percent (xlsxwriter accepts 10 to 400).

100
tab_color str or None

Color of the sheet tab, a hex code such as "#FF0000" or a color name such as "red". None keeps the default.

None

Returns:

Type Description
Worksheet

The new worksheet, also registered in ws_dict and in the pandas writer so that DataFrames can be written to it.

Notes

cell_scale True or a tuple overwrites default_row_height and default_col_width, which later charts and images use to convert their size into cells.

Source code in ExcelMaster/ExcelMaster.py
def add_worksheet(self, name, hide_grid = True, reset_loc = True, cell_scale = True, auto_fit = False, zoom_perc = 100, tab_color = None):
    """Add a worksheet.

    Parameters
    ----------
    name : str
        Name of the worksheet (unique in the workbook, at most 31 characters).
    hide_grid : bool, default True
        If True, hide the gridlines on screen and in print.
    reset_loc : bool, default True
        If True, reset the cursor to (0, 0).
    cell_scale : bool or tuple of float, default True
        ``True`` writes an explicit size for every row and column of the sheet (20 px high and 64 px wide), which takes a
        few seconds and adds a few MB to the file; a tuple ``(row_scale, col_scale)`` multiplies that size (for example
        ``(1, 2)`` makes the columns twice as wide). Any other value (``False``, None) leaves the sizes of the sheet and
        ``default_row_height`` / ``default_col_width`` untouched.
    auto_fit : bool, default False
        If True, widen the columns of the sheet to fit their content. The widths are fitted by ``close_workbook`` (with
        ``worksheet.autofit()`` of xlsxwriter), once everything is written, because xlsxwriter measures the cells that
        exist at that moment. A column is only widened, never narrowed below the width the sheet already has, so with
        ``cell_scale=True`` the 64 px base width is the minimum.
    zoom_perc : int, default 100
        Zoom of the worksheet in percent (xlsxwriter accepts 10 to 400).
    tab_color : str or None, default None
        Color of the sheet tab, a hex code such as ``"#FF0000"`` or a color name such as ``"red"``. None keeps the default.

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        The new worksheet, also registered in ``ws_dict`` and in the pandas writer so that DataFrames can be written to it.

    Notes
    -----
    ``cell_scale`` True or a tuple overwrites ``default_row_height`` and ``default_col_width``, which later charts and
    images use to convert their size into cells.
    """
    ws = self.workbook.add_worksheet(name)

    if hide_grid:
        ws.hide_gridlines(2)

    if reset_loc:
        self.reset_curr_loc()

    if isinstance(cell_scale, tuple):
        self.set_cell_size(ws, cell_scale)
    if cell_scale is True:
        self.set_cell_size(ws)

    if auto_fit:
        self._autofit_sheets.append(ws)

    if tab_color:
        ws.set_tab_color(tab_color)

    ws.set_zoom(zoom_perc)
    self.ws_dict[name] = ws
    self.engine.sheets[name] = ws
    return ws

reset_curr_loc

reset_curr_loc(loc=(0, 0))

Reset Current Location.

Parameters:

Name Type Description Default
loc tuple of int

New cursor position (row, col), zero-based.

(0, 0)

Returns:

Type Description
int

Always 0.

Source code in ExcelMaster/ExcelMaster.py
def reset_curr_loc(self, loc = (0, 0)):
    """Reset Current Location.

    Parameters
    ----------
    loc : tuple of int, default (0, 0)
        New cursor position ``(row, col)``, zero-based.

    Returns
    -------
    int
        Always 0.
    """

    self.curr_row = loc[0]
    self.curr_col = loc[1]
    return 0

set_cell_size

set_cell_size(worksheet, size_scale=(1, 1))

Set Cell Size in Scale.

Sets the pixel height of every row (all 1,048,576) and the pixel width of every column of the worksheet, and stores the sizes in default_row_height and default_col_width, which later chart and image sizes are converted with.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet to resize.

required
size_scale tuple of float

(row_scale, col_scale) multiplying the base cell size of 20 px high by 64 px wide. A value that is not a 2-tuple leaves default_row_height and default_col_width as they are, and these are applied again.

(1, 1)

Returns:

Type Description
int

Always 0.

Notes

The row height is set as the default row height of the sheet. Setting the 1,048,576 rows one by one made xlsxwriter keep a record for each of them: about 0.5 GB of memory and 8 seconds per worksheet, and 15 MB per sheet in the file, so a report of twenty sheets could exhaust the memory of the machine.

Source code in ExcelMaster/ExcelMaster.py
def set_cell_size(self, worksheet, size_scale = (1, 1)):
    """Set Cell Size in Scale.

    Sets the pixel height of every row (all 1,048,576) and the pixel width of every column of the worksheet, and stores the
    sizes in ``default_row_height`` and ``default_col_width``, which later chart and image sizes are converted with.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet to resize.
    size_scale : tuple of float, default (1, 1)
        ``(row_scale, col_scale)`` multiplying the base cell size of 20 px high by 64 px wide. A value that is not a
        2-tuple leaves ``default_row_height`` and ``default_col_width`` as they are, and these are applied again.

    Returns
    -------
    int
        Always 0.

    Notes
    -----
    The row height is set as the default row height of the sheet. Setting the 1,048,576 rows one by one made
    xlsxwriter keep a record for each of them: about 0.5 GB of memory and 8 seconds per worksheet, and 15 MB per
    sheet in the file, so a report of twenty sheets could exhaust the memory of the machine.
    """

    if isinstance(size_scale, tuple) and len(size_scale) == 2:
        self._reset_cell_size()
        self.default_row_height = self.default_row_height * size_scale[0]
        self.default_col_width =  self.default_col_width * size_scale[1]

    # xlsxwriter works in points for rows: 1 px = 0.75 pt (what ``set_row_pixels`` converts with).
    worksheet.set_default_row(self.default_row_height * 0.75)
    worksheet.set_column_pixels(0, self.max_ncols - 1, width=self.default_col_width)
    return 0

get_curr_loc

get_curr_loc(toCell=False)

Get Current Location in worksheet.

Parameters:

Name Type Description Default
toCell bool

If True, return the cursor as an A1-style cell reference instead of a tuple.

False

Returns:

Type Description
tuple of int or str

(curr_row, curr_col) (zero-based), or for example "C4" when toCell is True.

Source code in ExcelMaster/ExcelMaster.py
def get_curr_loc(self, toCell = False):
    """Get Current Location in worksheet.

    Parameters
    ----------
    toCell : bool, default False
        If True, return the cursor as an A1-style cell reference instead of a tuple.

    Returns
    -------
    tuple of int or str
        ``(curr_row, curr_col)`` (zero-based), or for example ``"C4"`` when ``toCell`` is True.
    """
    if toCell:
        return xl_rowcol_to_cell(self.curr_row, self.curr_col)
    return (self.curr_row, self.curr_col)

set_border_line

set_border_line(worksheet, valuerange, border_line=1)

Set border line for a range of cells.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the border.

required
valuerange str or list or tuple

Cell range in A1 notation ("B2:D6") or [first_row, first_col, last_row, last_col] (zero-based).

required
border_line int

xlsxwriter border style index, used for the top, bottom, left and right border of every cell of the range (1 thin, 2 medium, 5 thick, ...).

1

Returns:

Type Description
int

Always 0.

Notes

The border is applied as a conditional format (through set_cell_format).

Source code in ExcelMaster/ExcelMaster.py
def set_border_line(self, worksheet, valuerange, border_line = 1):
    """Set border line for a range of cells.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the border.
    valuerange : str or list or tuple
        Cell range in A1 notation (``"B2:D6"``) or ``[first_row, first_col, last_row, last_col]`` (zero-based).
    border_line : int, default 1
        xlsxwriter border style index, used for the top, bottom, left and right border of every cell of the range
        (1 thin, 2 medium, 5 thick, ...).

    Returns
    -------
    int
        Always 0.

    Notes
    -----
    The border is applied as a conditional format (through ``set_cell_format``).
    """
    border_fmt = self.workbook.add_format({'bottom': border_line, 'top': border_line, 'left': border_line, 'right': border_line})
    self.set_cell_format(worksheet = worksheet, cell_range = valuerange, cformat = border_fmt)
    return 0

merge_col

merge_col(worksheet, loc=None, nrows=1, ncols=1, text='', skipby='row', cformat='BLUE_H4', retCellRange=None)

Merge columns in a single row.

Merges the block of nrows x ncols cells that starts at loc (one row by default) and writes text in it.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet to write on.

required
loc tuple of int or None

(row, col) (zero-based) of the top-left cell. None starts at the cursor.

None
nrows int

Number of rows to merge.

1
ncols int

Number of columns to merge.

1
text str

Text written in the merged cell.

""
skipby str or None

Cursor move after the write: 'row' sets curr_row to start_row + nrows + gap_number, 'col' sets curr_col to start_col + ncols + gap_number; any other value (for example None) leaves the cursor unchanged.

'row'
cformat str

Name of a format in dict_cell_format. An unknown name, and also an xlsxwriter Format object, raises KeyError.

'BLUE_H4'
retCellRange str or None

"text" returns the written range in A1 notation (for example "A1:C1"); "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0.

None

Returns:

Type Description
int or str or list of int

0, or the written range as selected by retCellRange.

Notes

A block of one row and one column is written as a plain cell, because xlsxwriter cannot merge a single cell.

Source code in ExcelMaster/ExcelMaster.py
def merge_col(self, worksheet, loc = None, nrows = 1, ncols = 1, text = "", skipby = 'row', cformat = 'BLUE_H4', retCellRange = None):
    """Merge columns in a single row.

    Merges the block of ``nrows`` x ``ncols`` cells that starts at ``loc`` (one row by default) and writes ``text`` in it.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the top-left cell. None starts at the cursor.
    nrows : int, default 1
        Number of rows to merge.
    ncols : int, default 1
        Number of columns to merge.
    text : str, default ""
        Text written in the merged cell.
    skipby : str or None, default 'row'
        Cursor move after the write: ``'row'`` sets ``curr_row`` to ``start_row + nrows + gap_number``, ``'col'`` sets
        ``curr_col`` to ``start_col + ncols + gap_number``; any other value (for example None) leaves the cursor unchanged.
    cformat : str, default 'BLUE_H4'
        Name of a format in ``dict_cell_format``. An unknown name, and also an xlsxwriter ``Format`` object, raises
        ``KeyError``.
    retCellRange : str or None, default None
        ``"text"`` returns the written range in A1 notation (for example ``"A1:C1"``); ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

    Returns
    -------
    int or str or list of int
        0, or the written range as selected by ``retCellRange``.

    Notes
    -----
    A block of one row and one column is written as a plain cell, because xlsxwriter cannot merge a single cell.
    """

    start_row = loc[0] if loc else self.curr_row
    start_col = loc[1] if loc else self.curr_col
    written_range = [start_row, start_col, start_row + nrows - 1, start_col + ncols - 1]

    if nrows == 1 and ncols == 1:
        # xlsxwriter refuses to merge a single cell and writes nothing, so a
        # one-column table used to lose its title; write the cell directly.
        worksheet.write(start_row, start_col, text, self.dict_cell_format[cformat])
    else:
        worksheet.merge_range(*written_range, text, self.dict_cell_format[cformat])

    if self.verbose:
        logging.info(f"Merged Cells: {self.to_cell_range_text(*written_range)}")

    # Skipped by Rows/Columns
    if skipby == 'row':
        self.curr_row = (start_row + nrows + self.gap_number)
    if skipby == 'col':
        self.curr_col = (start_col + ncols + self.gap_number)

    # Return written location by Cell Text/Value Range
    if retCellRange == "text":
        return self.to_cell_range_text(*written_range)
    if retCellRange == "value":
        return written_range

    return 0

write_dataframe

write_dataframe(worksheet, df, loc=None, title=None, index=False, header=True, skipby='row', titleformat='BLUE_H4', headerformat='TABLE_HEADER', valueformat='----', retCellRange=None)

Write a dataframe to excel file.

Writes the optional title row (merged across the table), the header and the values with DataFrame.to_excel, then formats the header and the value cells.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet to write on. The table goes through the pandas writer, which finds the sheet by its name.

required
df DataFrame

Table to write.

required
loc tuple of int or None

(row, col) (zero-based) of the top-left cell, the title row if there is one. None starts at the cursor.

None
title str or None

Title written in a merged row above the table (format titleformat). None or an empty string writes no title row.

None
index bool

If True, write the index as the leading column(s), one per index level.

False
header bool

If True, write the column names, one header row per column level.

True
skipby str or None

Cursor move after the write: 'row' sets curr_row to start_row + nrows + gap_number (nrows counts the title and header rows), 'col' sets curr_col to start_col + ncols + gap_number (ncols counts the index columns); any other value (for example None) leaves the cursor unchanged.

'row'
titleformat str

Name of a format in dict_cell_format for the title (a name only: a Format object raises KeyError).

"BLUE_H4"
headerformat str or Format

Format of the header cells: a name in dict_cell_format or a Format object.

"TABLE_HEADER"
valueformat str or Format

Format of every value cell, as a name in dict_cell_format or a Format object. It applies to all columns, so a percentage format would also turn count columns into percentages; format single columns afterwards with set_cell_format.

"----"
retCellRange str or None

"text" returns the written range in A1 notation (for example "A1:D6"); "value" returns [first_row, first_col, last_row, last_col] (zero-based), title and header included; None returns 0.

None

Returns:

Type Description
int or str or list of int

0, or the written range as selected by retCellRange.

Notes

The header and value formats are applied as conditional formats (see set_cell_format).

Source code in ExcelMaster/ExcelMaster.py
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):
    """Write a dataframe to excel file.

    Writes the optional title row (merged across the table), the header and the values with ``DataFrame.to_excel``, then
    formats the header and the value cells.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet to write on. The table goes through the pandas writer, which finds the sheet by its name.
    df : pandas.DataFrame
        Table to write.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the top-left cell, the title row if there is one. None starts at the cursor.
    title : str or None, default None
        Title written in a merged row above the table (format ``titleformat``). None or an empty string writes no title
        row.
    index : bool, default False
        If True, write the index as the leading column(s), one per index level.
    header : bool, default True
        If True, write the column names, one header row per column level.
    skipby : str or None, default 'row'
        Cursor move after the write: ``'row'`` sets ``curr_row`` to ``start_row + nrows + gap_number`` (``nrows`` counts
        the title and header rows), ``'col'`` sets ``curr_col`` to ``start_col + ncols + gap_number`` (``ncols`` counts
        the index columns); any other value (for example None) leaves the cursor unchanged.
    titleformat : str, default "BLUE_H4"
        Name of a format in ``dict_cell_format`` for the title (a name only: a ``Format`` object raises ``KeyError``).
    headerformat : str or xlsxwriter.format.Format, default "TABLE_HEADER"
        Format of the header cells: a name in ``dict_cell_format`` or a ``Format`` object.
    valueformat : str or xlsxwriter.format.Format, default "----"
        Format of every value cell, as a name in ``dict_cell_format`` or a ``Format`` object. It applies to all columns, so
        a percentage format would also turn count columns into percentages; format single columns afterwards with
        ``set_cell_format``.
    retCellRange : str or None, default None
        ``"text"`` returns the written range in A1 notation (for example ``"A1:D6"``); ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based), title and header included; None returns 0.

    Returns
    -------
    int or str or list of int
        0, or the written range as selected by ``retCellRange``.

    Notes
    -----
    The header and value formats are applied as conditional formats (see ``set_cell_format``).
    """

    start_row = loc[0] if loc else self.curr_row
    start_col = loc[1] if loc else self.curr_col

    ncols = df.shape[1] 
    nrows = df.shape[0]

    ## Get Number of Index Columns
    index_ncols = 0
    if index:
        index_ncols = len(df.index.names)
        ncols += index_ncols

    ## Get Number of Header Rows
    header_nrows = 0
    if header:
        header_nrows = len(df.columns.names)
        nrows += header_nrows

    ## Get Number of Title Rows
    title_nrows = 0
    if title:
        title_nrows = 1
        nrows += title_nrows

    ## Get Header Range (Include Index)
    if header:
        header_start_row = (start_row + title_nrows)
        header_start_col = start_col
        header_end_row = max((start_row + header_nrows - 1), header_start_row)
        header_end_col = (start_col + ncols - 1)
        header_range = self.to_cell_range_text(header_start_row, header_start_col, 
                                               header_end_row, header_end_col)

    ## Write DataFrame
    if title:
        self.merge_col(worksheet = worksheet, loc = loc, ncols = ncols, cformat=titleformat, 
                       text = title, skipby = None)
        df.to_excel(self.engine, sheet_name = worksheet.name, startrow = start_row + title_nrows, 
                    startcol = start_col, header = header, index = index)
    else:
        df.to_excel(self.engine, sheet_name = worksheet.name, startrow = start_row, 
                    startcol = start_col, header = header, index = index)


    ## Get Value Range (Include Index)
    value_start_row = (start_row + title_nrows + header_nrows)
    value_start_col = start_col
    value_end_row = (start_row + nrows - 1)
    value_end_col = (start_col + ncols - 1)
    value_range = [value_start_row, value_start_col, value_end_row, value_end_col]

    ## Set Format
    self.set_cell_format(worksheet = worksheet, cell_range = value_range, cformat = valueformat)
    if header:
        self.set_cell_format(worksheet = worksheet, cell_range = header_range, cformat = headerformat)

    written_range = [start_row, start_col, (start_row + nrows - 1), (start_col + ncols - 1)]

    if self.verbose:
        logging.info(f"Table Written in Cell Range: {self.to_cell_range_text(*written_range)}")

    if skipby == 'row':
        self.curr_row = (start_row + nrows + self.gap_number)
    if skipby == 'col':
        self.curr_col = (start_col + ncols + self.gap_number)

    if retCellRange == "text":
        return self.to_cell_range_text(*written_range)
    if retCellRange == "value":
        return written_range

    return 0

insert_image

insert_image(worksheet, figPath, figScale=(1, 1), loc=None, skipby='row', retCellRange=None)

Insert an image to the sheet.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet to write on.

required
figPath str

Path of the image file (for example PNG or JPEG).

required
figScale tuple of float

(x_scale, y_scale) factors for the width and the height of the image: 1 keeps the size, 0.8 shrinks it by 20 percent. They are factors, not pixels.

(1, 1)
loc tuple of int or None

(row, col) (zero-based) of the cell that holds the top-left corner of the image. None starts at the cursor.

None
skipby str or None

Cursor move after the write: 'row' moves the cursor below the image, 'col' to the right of it, by the size of the image in cells plus gap_number; any other value (for example None) leaves the cursor unchanged.

'row'
retCellRange str or None

"text" returns the covered range in A1 notation; "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0.

None

Returns:

Type Description
int or str or list of int

0, or the covered range as selected by retCellRange.

Notes

The size of the image in cells is computed from the scaled pixel size and default_row_height / default_col_width (rows rounded down, columns rounded up), so the returned range and the cursor move are approximate.

Source code in ExcelMaster/ExcelMaster.py
def insert_image(self, worksheet, figPath, figScale = (1, 1), loc = None, skipby = 'row', retCellRange = None):
    """Insert an image to the sheet.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    figPath : str
        Path of the image file (for example PNG or JPEG).
    figScale : tuple of float, default (1, 1)
        ``(x_scale, y_scale)`` factors for the width and the height of the image: 1 keeps the size, 0.8 shrinks it by 20
        percent. They are factors, not pixels.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the cell that holds the top-left corner of the image. None starts at the cursor.
    skipby : str or None, default 'row'
        Cursor move after the write: ``'row'`` moves the cursor below the image, ``'col'`` to the right of it, by the size
        of the image in cells plus ``gap_number``; any other value (for example None) leaves the cursor unchanged.
    retCellRange : str or None, default None
        ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

    Returns
    -------
    int or str or list of int
        0, or the covered range as selected by ``retCellRange``.

    Notes
    -----
    The size of the image in cells is computed from the scaled pixel size and ``default_row_height`` /
    ``default_col_width`` (rows rounded down, columns rounded up), so the returned range and the cursor move are
    approximate.
    """
    start_row = loc[0] if loc else self.curr_row
    start_col = loc[1] if loc else self.curr_col

    start_cell = xl_rowcol_to_cell(start_row, start_col)
    figsize_in_cells = self._get_image_size(figPath = figPath, figScale = figScale, retSizeInCell = True)

    worksheet.insert_image(start_cell, figPath, {"x_scale": figScale[0], "y_scale": figScale[1]})

    # Skipped by Rows/Columns
    if skipby == 'row':
        self.curr_row = (start_row + int(figsize_in_cells[1]) + self.gap_number)
    if skipby == 'col':
        self.curr_col = (start_col + int(figsize_in_cells[0]) + self.gap_number)

    written_range = [start_row, start_col, 
                     start_row + int(figsize_in_cells[1]), 
                     start_col + int(figsize_in_cells[0])]

    # Return written location by Cell Text/Value Range
    if retCellRange == "text":
        return self.to_cell_range_text(*written_range)
    if retCellRange == "value":
        return written_range

    if self.verbose:
        logging.info(f"Image Written in Cell Range: {self.to_cell_range_text(*written_range)}")

    return 0

write_chart

write_chart(worksheet, df, y_list, x=None, title='', chart_size=(30, 13), chart_type='line', y_axis_range=(None, None), y_num_format=None, y2_axis=False, loc=None, retChart=False, skipby='row', outputData=False, retCellRange=None, xy_axes_name=('', ''), major_gridlines=False, legend='bottom', line_marker='circle', line_type='solid', chart_style=None, append_to_chart=None)

Write line chart to Excel worksheet.

Builds an xlsxwriter chart from df and inserts it at loc (or at the cursor). Despite the summary, chart_type also gives column, stacked column and pie charts. The source data of the chart are written to a hidden worksheet named __CHRT_DATA_<N>, or to worksheet itself with outputData=True.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the chart.

required
df DataFrame

Source data, one row per category.

required
y_list list of str

Columns of df to plot: one series per column, named after the column.

required
x str or list of str or None

Column(s) of df that give the category labels (several columns give multi-level labels). None uses the index of df (all its levels).

None
title str

Title of the chart.

""
chart_size tuple of int

(rows, columns) measured in worksheet cells, not pixels: the chart is chart_size[0] * default_row_height pixels high and chart_size[1] * default_col_width pixels wide.

(30, 13)
chart_type str or dict

"line", "column", "stacked_column" or "pie"; or an options dict for workbook.add_chart such as {"type": "line"}.

"line"
y_axis_range tuple

(min, max) of the value axis; None entries (or None instead of the tuple) mean automatic scaling.

(None, None)
y_num_format str or None

Number format of the value axis, for example "0.00%". None sets no number format.

None
y2_axis bool

If True, plot the series on the secondary y axis; the axis settings (range, format, title, gridlines) then apply to that axis.

False
loc tuple of int or None

(row, col) (zero-based) of the top-left cell of the block. None starts at the cursor.

None
retChart bool

If True, return the chart object instead of inserting it: nothing is inserted and the cursor does not move. Use it with write_combined_chart or append_to_chart.

False
skipby str or None

Cursor move after the insertion: 'row' moves the cursor below the chart, 'col' to the right of it, by the size of the chart (and of the data table with outputData) plus gap_number; any other value (for example None) leaves the cursor unchanged.

"row"
outputData bool

If True, write the table of chart data to worksheet at the insertion point and put the chart below it; the hidden data sheet that is created anyway then stays empty.

False
retCellRange str or None

"text" returns the covered range in A1 notation; "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0. The range is approximate.

None
xy_axes_name tuple of str

(x axis title, y axis title).

("", "")
major_gridlines bool

If True, show the major gridlines of the value axis.

False
legend str

xlsxwriter legend position: "top", "bottom", "left", "right", "overlay_left", "overlay_right", or "none" to hide the legend.

"bottom"
line_marker str

Marker type of every series (xlsxwriter marker type, for example "circle", "square", "none").

"circle"
line_type str

Dash type of the line of every series (xlsxwriter dash_type, for example "solid", "dash", "long_dash").

"solid"
chart_style int or None

xlsxwriter built-in chart style number (chart.set_style). None keeps the default style.

None
append_to_chart Chart or None

Existing chart (obtained with retChart=True) that receives the series instead of a new chart. It must be of the type given by chart_type (line, column or stacked column, pie); otherwise a warning is issued and a new chart is used.

None

Returns:

Type Description
xlsxwriter.chart.Chart or int or str or list of int

The chart when retChart is True; otherwise 0, or the range covered by the chart as selected by retCellRange.

Raises:

Type Description
ValueError

If chart_type is a string other than "line", "column", "stacked_column" and "pie".

Notes

Every call adds a hidden worksheet __CHRT_DATA_<N> to the workbook (it is registered in ws_dict).

Source code in ExcelMaster/ExcelMaster.py
def write_chart(self, worksheet, df, y_list, x = None, title = "", 
                chart_size = (30, 13), chart_type = "line",
                y_axis_range = (None, None), y_num_format = None,
                y2_axis = False, loc = None, retChart = False, 
                skipby = "row", outputData = False, retCellRange = None,
                xy_axes_name = ("", ""), major_gridlines = False,
                legend = "bottom", line_marker = "circle", line_type = "solid", 
                chart_style = None, append_to_chart = None): 
    """Write line chart to Excel worksheet.

    Builds an xlsxwriter chart from ``df`` and inserts it at ``loc`` (or at the cursor). Despite the summary, ``chart_type``
    also gives column, stacked column and pie charts. The source data of the chart are written to a hidden worksheet named
    ``__CHRT_DATA_<N>``, or to ``worksheet`` itself with ``outputData=True``.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the chart.
    df : pandas.DataFrame
        Source data, one row per category.
    y_list : list of str
        Columns of ``df`` to plot: one series per column, named after the column.
    x : str or list of str or None, default None
        Column(s) of ``df`` that give the category labels (several columns give multi-level labels). None uses the index
        of ``df`` (all its levels).
    title : str, default ""
        Title of the chart.
    chart_size : tuple of int, default (30, 13)
        ``(rows, columns)`` measured in worksheet cells, not pixels: the chart is ``chart_size[0] * default_row_height``
        pixels high and ``chart_size[1] * default_col_width`` pixels wide.
    chart_type : str or dict, default "line"
        ``"line"``, ``"column"``, ``"stacked_column"`` or ``"pie"``; or an options dict for ``workbook.add_chart`` such as
        ``{"type": "line"}``.
    y_axis_range : tuple, default (None, None)
        ``(min, max)`` of the value axis; None entries (or None instead of the tuple) mean automatic scaling.
    y_num_format : str or None, default None
        Number format of the value axis, for example ``"0.00%"``. None sets no number format.
    y2_axis : bool, default False
        If True, plot the series on the secondary y axis; the axis settings (range, format, title, gridlines) then apply
        to that axis.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the top-left cell of the block. None starts at the cursor.
    retChart : bool, default False
        If True, return the chart object instead of inserting it: nothing is inserted and the cursor does not move. Use it
        with ``write_combined_chart`` or ``append_to_chart``.
    skipby : str or None, default "row"
        Cursor move after the insertion: ``'row'`` moves the cursor below the chart, ``'col'`` to the right of it, by the
        size of the chart (and of the data table with ``outputData``) plus ``gap_number``; any other value (for example
        None) leaves the cursor unchanged.
    outputData : bool, default False
        If True, write the table of chart data to ``worksheet`` at the insertion point and put the chart below it; the
        hidden data sheet that is created anyway then stays empty.
    retCellRange : str or None, default None
        ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0. The range is approximate.
    xy_axes_name : tuple of str, default ("", "")
        ``(x axis title, y axis title)``.
    major_gridlines : bool, default False
        If True, show the major gridlines of the value axis.
    legend : str, default "bottom"
        xlsxwriter legend position: ``"top"``, ``"bottom"``, ``"left"``, ``"right"``, ``"overlay_left"``,
        ``"overlay_right"``, or ``"none"`` to hide the legend.
    line_marker : str, default "circle"
        Marker type of every series (xlsxwriter ``marker`` type, for example ``"circle"``, ``"square"``, ``"none"``).
    line_type : str, default "solid"
        Dash type of the line of every series (xlsxwriter ``dash_type``, for example ``"solid"``, ``"dash"``,
        ``"long_dash"``).
    chart_style : int or None, default None
        xlsxwriter built-in chart style number (``chart.set_style``). None keeps the default style.
    append_to_chart : xlsxwriter.chart.Chart or None, default None
        Existing chart (obtained with ``retChart=True``) that receives the series instead of a new chart. It must be of the
        type given by ``chart_type`` (line, column or stacked column, pie); otherwise a warning is issued and a new chart
        is used.

    Returns
    -------
    xlsxwriter.chart.Chart or int or str or list of int
        The chart when ``retChart`` is True; otherwise 0, or the range covered by the chart as selected by
        ``retCellRange``.

    Raises
    ------
    ValueError
        If ``chart_type`` is a string other than ``"line"``, ``"column"``, ``"stacked_column"`` and ``"pie"``.

    Notes
    -----
    Every call adds a hidden worksheet ``__CHRT_DATA_<N>`` to the workbook (it is registered in ``ws_dict``).
    """

    if isinstance(chart_type, str):
        if chart_type == "line":
            chart_type = {'type': 'line'}
        elif chart_type == "column":
            chart_type = {'type': 'column'}
        elif chart_type == "stacked_column":
            chart_type = {'type': 'column', "subtype": "stacked"}
        elif chart_type == "pie":
            chart_type = {"type": "pie"}
        else:
            raise ValueError(" Please select one from 'line', 'column', 'stacked_column' or 'pie'. ")

    start_row = loc[0] if loc else self.curr_row
    start_col = loc[1] if loc else self.curr_col

    y_axis_range = self.__convert_to_none_tuple(y_axis_range)

    df_t = self._transpose_df_for_chart(df = df, y_list = y_list, x = x)
    if x is None:
        x = list(df.index.names)
    if isinstance(x, str):
        x = [x]
    y = "index" if len(x) == 1 else tuple(["index"] + [''] * (len(x) - 1))
    x_list = [x for x in df_t.columns if x != y]

    data_nrows = df_t.shape[0]
    data_ncols = df_t.shape[1]

    raw_data_ws = self.__add_chart_data_tab("__CHRT_DATA_")

    ## if y not given, then use index value as y series.
    data_index_as_y = True
    if y is None:
        sel_df = df_t[[*x_list]]
    else:
        sel_df = df_t[[y, *x_list]]
        sel_df = sel_df.set_index(y)
        sel_df.index.name = None

    ## if output chart data to current worksheet.
    if outputData:
        raw_data_ws = worksheet
        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))
        chart_loc = (df_range[2] + self.gap_number, df_range[1])
    else:
        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))
        chart_loc = (start_row, start_col)

    x_count = len(x_list)
    nrows = df_t.shape[0]

    data_row_anchor = df_range[0]
    data_col_anchor = df_range[1]
    row_shift = 0

    category_range = [df_range[0], df_range[1] + 1, 
                      df_range[0] + (len(x) - 1), df_range[3]]

    if self.verbose:
        logging.info(f"Category Cell Range: {self.to_cell_range_text(*category_range)}")

    chart = self.workbook.add_chart(chart_type)
    if append_to_chart is not None:
        if self.__validate_input_chart_obj(chart_type = chart_type["type"], input_chart = append_to_chart):
            chart = append_to_chart
        else:
            warnings.warn("WARNING: Can only append data to the chart that has the same chart type as your given one.")

    value_start = len(x) + 1 if len(x) > 1 else len(x)
    for row_shift in range(value_start, value_start + nrows, 1):

        value_range = [data_row_anchor + row_shift, data_col_anchor + 1, 
                       data_row_anchor + row_shift, data_col_anchor + x_count]

        chart.add_series({
            'name':        [raw_data_ws.name, (data_row_anchor + row_shift), data_col_anchor],
            'categories':  [raw_data_ws.name, *category_range],
            'values':      [raw_data_ws.name, *value_range],
            'marker':      {'type': line_marker},
            "line" :       {'dash_type': line_type},
            'data_labels': {'percentage': True} if chart_type['type'] == 'pie' else None,
            'y2_axis':     y2_axis,
        })
        if self.verbose:
            logging.info("Values Cell Range: %s", self.to_cell_range_text(*value_range))

    chart.set_title({'name': title})
    chart.set_legend({'position': str(legend)})
    chart.set_size({"height": chart_size[0] * self.default_row_height, # by row 15
                    "width": chart_size[1] * self.default_col_width}) # by col 8.43

    chart.set_x_axis({"name": xy_axes_name[0]})

    set_y_axis_dict = {"name": xy_axes_name[1], 
                       "major_gridlines": {"visible": int(major_gridlines)},
                       'min': y_axis_range[0], 
                       'max': y_axis_range[1],
                       'num_format': y_num_format}
    if y2_axis:
        chart.set_y2_axis(set_y_axis_dict)
    else:
        chart.set_y_axis(set_y_axis_dict)

    # Setting Chart Style
    if chart_style:
        chart.set_style(chart_style)

    # Need to Return Chart before Insert it to the worksheet.
    if retChart:
        return chart

    worksheet.insert_chart(
        chart_loc[0], 
        chart_loc[1], 
        chart
    )

    written_range = [start_row, start_col, int(chart_loc[0] + chart_size[0]), int(chart_loc[1] + chart_size[1])]
    if outputData:

        written_range = [start_row, start_col, 
                         (start_row + data_nrows + self.gap_number + chart_size[0]),
                         max(df_range[3], start_col + chart_size[1])]

    if skipby == 'row':
        if outputData:
            self.curr_row = (start_row + data_nrows + self.gap_number + chart_size[0] + self.gap_number - 1)
        else:
            self.curr_row = (start_row + chart_size[0] + self.gap_number - 1)
    if skipby == 'col':
        self.curr_col = (start_col + int(chart_size[1]) + self.gap_number)

    if self.verbose:
        logging.info(f"Chart Written in Cell Range: {self.to_cell_range_text(*written_range)}")

    if retCellRange == "text":
        return self.to_cell_range_text(*written_range)
    if retCellRange == "value":
        return written_range

    return 0

write_combined_chart

write_combined_chart(worksheet, chart1, chart2, loc=None, chart_size=(30, 13), skipby='row', retCellRange=None)

Combined two chart objects and then write to Excel worksheet.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the combined chart.

required
chart1 Chart

Primary chart, obtained with retChart=True. chart2 is combined into it and it is the object inserted, so its title, size and primary axis apply.

required
chart2 Chart or None

Secondary chart, obtained with retChart=True, combined into chart1 (xlsxwriter supports only some combinations, for example a column chart with a line chart). None inserts chart1 alone.

required
loc tuple of int or None

(row, col) (zero-based) of the top-left cell. None starts at the cursor.

None
chart_size tuple of int

(rows, columns) in cells. It is used only to compute the returned range and the cursor move; it does not resize the chart, whose size was set when the charts were created.

(30, 13)
skipby str or None

Cursor move after the insertion: 'row' moves the cursor below the chart, 'col' to the right of it, by chart_size plus gap_number; any other value (for example None) leaves the cursor unchanged.

"row"
retCellRange str or None

"text" returns the covered range in A1 notation; "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0.

None

Returns:

Type Description
int or str or list of int

0, or the covered range as selected by retCellRange.

Source code in ExcelMaster/ExcelMaster.py
def write_combined_chart(self, worksheet, chart1, chart2, 
                         loc = None, chart_size = (30, 13), 
                         skipby = "row", retCellRange = None):
    """Combined two chart objects and then write to Excel worksheet.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the combined chart.
    chart1 : xlsxwriter.chart.Chart
        Primary chart, obtained with ``retChart=True``. ``chart2`` is combined into it and it is the object inserted, so
        its title, size and primary axis apply.
    chart2 : xlsxwriter.chart.Chart or None
        Secondary chart, obtained with ``retChart=True``, combined into ``chart1`` (xlsxwriter supports only some
        combinations, for example a column chart with a line chart). None inserts ``chart1`` alone.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the top-left cell. None starts at the cursor.
    chart_size : tuple of int, default (30, 13)
        ``(rows, columns)`` in cells. It is used only to compute the returned range and the cursor move; it does not
        resize the chart, whose size was set when the charts were created.
    skipby : str or None, default "row"
        Cursor move after the insertion: ``'row'`` moves the cursor below the chart, ``'col'`` to the right of it, by
        ``chart_size`` plus ``gap_number``; any other value (for example None) leaves the cursor unchanged.
    retCellRange : str or None, default None
        ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

    Returns
    -------
    int or str or list of int
        0, or the covered range as selected by ``retCellRange``.
    """

    start_row = loc[0] if loc else self.curr_row
    start_col = loc[1] if loc else self.curr_col

    if chart2 is not None:
        chart1.combine(chart2)
    worksheet.insert_chart(xl_rowcol_to_cell(start_row, start_col), chart1)

    written_range = [start_row, start_col, (start_row + chart_size[0]), (start_col + chart_size[1])]

    if skipby == 'row':
        self.curr_row = (start_row + chart_size[0] + self.gap_number - 1)
    if skipby == 'col':
        self.curr_col = (start_col + chart_size[1] + self.gap_number)

    if self.verbose:
        logging.info(f"Chart Written in Cell Range: {self.to_cell_range_text(*written_range)}")

    if retCellRange == "text":
        return self.to_cell_range_text(*written_range)
    if retCellRange == "value":
        return written_range

    return 0

write_duo_chart

write_duo_chart(worksheet, df, y1_list, y2_list=None, x=None, c1_type='column', c2_type='line', y1_axis_range=(0, 1), y2_axis_range=None, y1_num_format=None, y2_num_format=None, y1_line_marker='circle', y2_line_marker='circle', y1_line_type='solid', y2_line_type='solid', loc=None, title='', chart_size=(30, 13), xy_axes_name=('', ''), major_gridlines=False, retChart=False, retCellRange=None, skipby='row')

Write duo-chart sharing the same x axis.

Builds two charts from df and x with write_chart (the first from y1_list on the primary y axis, the second from y2_list on the secondary y axis), combines them and inserts the result. Typical use: columns for counts on the left axis and a line for a rate on the right axis. Without y2_list only the first chart is built and inserted.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet that receives the chart.

required
df DataFrame

Source data, one row per category.

required
y1_list list of str

Columns drawn on the primary y axis, in the chart type c1_type.

required
y2_list list of str

Columns drawn on the secondary y axis, in the chart type c2_type. With the default None there is no second chart and no secondary axis: the first chart is inserted alone.

None
x str or list of str or None

Column(s) that give the category labels, as in write_chart. None uses the index of df.

None
c1_type str or dict

Type of the first chart: "line", "column", "stacked_column", "pie" or an options dict for workbook.add_chart.

"column"
c2_type str or dict

Type of the second chart, as c1_type.

"line"
y1_axis_range tuple or None

(min, max) of the primary y axis. The default fixes the axis to 0 - 1, which suits rates but clips counts; pass None (or (None, None)) for automatic scaling.

(0, 1)
y2_axis_range tuple or None

(min, max) of the secondary y axis. None uses y1_axis_range.

None
y1_num_format str or None

Number format of the primary y axis, for example "0.00%".

None
y2_num_format str or None

Number format of the secondary y axis.

None
y1_line_marker str

Marker type of the series of the first chart.

"circle"
y2_line_marker str

Marker type of the series of the second chart.

"circle"
y1_line_type str

Dash type of the lines of the first chart.

"solid"
y2_line_type str

Dash type of the lines of the second chart.

"solid"
loc tuple of int or None

(row, col) (zero-based) of the top-left cell. None starts at the cursor.

None
title str

Title of the chart (the combined chart shows the title of the first one).

""
chart_size tuple of int

(rows, columns) measured in worksheet cells, not pixels.

(30, 13)
xy_axes_name tuple of str

(x axis title, primary y title) or (x axis title, primary y title, secondary y title); with two entries the secondary y axis has no title.

("", "")
major_gridlines bool

If True, show the major gridlines of the primary y axis; the secondary axis never shows gridlines.

False
retChart bool

If True, return the tuple (chart1, chart2) instead of inserting anything (the cursor does not move).

False
retCellRange str or None

"text" returns the covered range in A1 notation; "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0.

None
skipby str or None

Cursor move after the insertion: 'row' moves the cursor below the chart, 'col' to the right of it; any other value (for example None) leaves the cursor unchanged.

"row"

Returns:

Type Description
tuple of xlsxwriter.chart.Chart or int or str or list of int

The tuple (chart1, chart2) when retChart is True (chart2 is None when y2_list is None); otherwise the result of write_combined_chart: 0, or the covered range as selected by retCellRange.

Notes

One hidden worksheet __CHRT_DATA_<N> per chart is added to the workbook.

Source code in ExcelMaster/ExcelMaster.py
def write_duo_chart(self, worksheet, df,
                    y1_list, y2_list = None, x = None,
                    c1_type = "column", c2_type = "line", 
                    y1_axis_range = (0, 1), y2_axis_range = None,
                    y1_num_format = None, y2_num_format = None,
                    y1_line_marker = "circle", y2_line_marker = "circle",
                    y1_line_type = "solid", y2_line_type = "solid",
                    loc = None, title = "", chart_size = (30, 13),
                    xy_axes_name = ("", ""), major_gridlines = False,
                    retChart = False, retCellRange = None,
                    skipby = "row"):
    """Write duo-chart sharing the same x axis.

    Builds two charts from ``df`` and ``x`` with ``write_chart`` (the first from ``y1_list`` on the primary y axis, the
    second from ``y2_list`` on the secondary y axis), combines them and inserts the result. Typical use: columns for counts
    on the left axis and a line for a rate on the right axis. Without ``y2_list`` only the first chart is built and
    inserted.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet that receives the chart.
    df : pandas.DataFrame
        Source data, one row per category.
    y1_list : list of str
        Columns drawn on the primary y axis, in the chart type ``c1_type``.
    y2_list : list of str, default None
        Columns drawn on the secondary y axis, in the chart type ``c2_type``. With the default None there is no second
        chart and no secondary axis: the first chart is inserted alone.
    x : str or list of str or None, default None
        Column(s) that give the category labels, as in ``write_chart``. None uses the index of ``df``.
    c1_type : str or dict, default "column"
        Type of the first chart: ``"line"``, ``"column"``, ``"stacked_column"``, ``"pie"`` or an options dict for
        ``workbook.add_chart``.
    c2_type : str or dict, default "line"
        Type of the second chart, as ``c1_type``.
    y1_axis_range : tuple or None, default (0, 1)
        ``(min, max)`` of the primary y axis. The default fixes the axis to 0 - 1, which suits rates but clips counts; pass
        None (or ``(None, None)``) for automatic scaling.
    y2_axis_range : tuple or None, default None
        ``(min, max)`` of the secondary y axis. None uses ``y1_axis_range``.
    y1_num_format : str or None, default None
        Number format of the primary y axis, for example ``"0.00%"``.
    y2_num_format : str or None, default None
        Number format of the secondary y axis.
    y1_line_marker : str, default "circle"
        Marker type of the series of the first chart.
    y2_line_marker : str, default "circle"
        Marker type of the series of the second chart.
    y1_line_type : str, default "solid"
        Dash type of the lines of the first chart.
    y2_line_type : str, default "solid"
        Dash type of the lines of the second chart.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the top-left cell. None starts at the cursor.
    title : str, default ""
        Title of the chart (the combined chart shows the title of the first one).
    chart_size : tuple of int, default (30, 13)
        ``(rows, columns)`` measured in worksheet cells, not pixels.
    xy_axes_name : tuple of str, default ("", "")
        ``(x axis title, primary y title)`` or ``(x axis title, primary y title, secondary y title)``; with two entries the
        secondary y axis has no title.
    major_gridlines : bool, default False
        If True, show the major gridlines of the primary y axis; the secondary axis never shows gridlines.
    retChart : bool, default False
        If True, return the tuple ``(chart1, chart2)`` instead of inserting anything (the cursor does not move).
    retCellRange : str or None, default None
        ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.
    skipby : str or None, default "row"
        Cursor move after the insertion: ``'row'`` moves the cursor below the chart, ``'col'`` to the right of it; any other
        value (for example None) leaves the cursor unchanged.

    Returns
    -------
    tuple of xlsxwriter.chart.Chart or int or str or list of int
        The tuple ``(chart1, chart2)`` when ``retChart`` is True (``chart2`` is None when ``y2_list`` is None); otherwise
        the result of ``write_combined_chart``: 0, or the covered range as selected by ``retCellRange``.

    Notes
    -----
    One hidden worksheet ``__CHRT_DATA_<N>`` per chart is added to the workbook.
    """

    start_row = loc[0] if loc else self.curr_row
    start_col = loc[1] if loc else self.curr_col

    y2_axis = True
    if y2_list is None:
        y2_axis = False

    if y2_axis_range is None:
        y2_axis_range = y1_axis_range

    xy_name = (xy_axes_name[0], xy_axes_name[1])
    xy2_name = (xy_axes_name[0], xy_axes_name[1])
    if y2_axis and len(xy_axes_name) >= 3:
        xy_name = (xy_axes_name[0], xy_axes_name[1])
        xy2_name = (xy_axes_name[0], xy_axes_name[2])
    if y2_axis and len(xy_axes_name) < 3:
        xy_name = (xy_axes_name[0], xy_axes_name[1])
        xy2_name = (xy_axes_name[0], "")

    chart1 = self.write_chart(df = df, 
                              x = x, 
                              y_list = y1_list, 
                              worksheet = worksheet, 
                              title = title,
                              chart_type = c1_type, 
                              chart_size = chart_size,
                              y_axis_range = y1_axis_range,
                              xy_axes_name = xy_name,
                              major_gridlines = major_gridlines,
                              y_num_format = y1_num_format,
                              line_type = y1_line_type,
                              line_marker = y1_line_marker,
                              retChart = True)

    chart2 = None
    if y2_axis:
        chart2 = self.write_chart(df = df, 
                                  x = x,
                                  y_list = y2_list, 
                                  worksheet=worksheet, 
                                  title = title, 
                                  chart_type = c2_type, 
                                  chart_size = chart_size,
                                  y_axis_range = y2_axis_range,
                                  y2_axis = y2_axis,
                                  xy_axes_name = xy2_name,
                                  major_gridlines = False,
                                  y_num_format = y2_num_format,
                                  line_type = y2_line_type,
                                  line_marker = y2_line_marker,
                                  retChart=True)

    # Need to Return Chart before Insert it to the worksheet.
    if retChart:
        return (chart1, chart2)

    cell_range = self.write_combined_chart(worksheet, 
                              chart1, chart2, 
                              loc = loc, chart_size = chart_size, 
                              skipby = skipby, retCellRange = retCellRange)

    return cell_range

write_text_by_dict

write_text_by_dict(worksheet, dict_cells)

Write text using Python dictionary.

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet to write on.

required
dict_cells dict

Maps a cell ("A1") or a range to merge ("M1:O2") to its content; formats are names in dict_cell_format:

  • range key with [text, format_name]: merge the range and write text in it;
  • cell key with [text, format_name]: write text with the format;
  • cell key with [[item, item, ...]] (a one-element list that holds a list): write a rich string. An item that starts with "~~~" is a format name ("~~~B") that applies to the text items that follow; the other items are text.
required

Returns:

Type Description
None

Nothing is returned. The cursor is not used or changed.

Source code in ExcelMaster/ExcelMaster.py
def write_text_by_dict(self, worksheet, dict_cells):
    """Write text using Python dictionary.

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    dict_cells : dict
        Maps a cell (``"A1"``) or a range to merge (``"M1:O2"``) to its content; formats are names in
        ``dict_cell_format``:

        * range key with ``[text, format_name]``: merge the range and write ``text`` in it;
        * cell key with ``[text, format_name]``: write ``text`` with the format;
        * cell key with ``[[item, item, ...]]`` (a one-element list that holds a list): write a rich string. An item that
          starts with ``"~~~"`` is a format name (``"~~~B"``) that applies to the text items that follow; the other items
          are text.

    Returns
    -------
    None
        Nothing is returned. The cursor is not used or changed.
    """
    for cell in dict_cells:
        list_contents = dict_cells[cell]

        if ':' in cell:
            text, cell_format = list_contents
            worksheet.merge_range(cell, text, self.dict_cell_format[cell_format])

        else:
            if len(list_contents) == 1:
                list_items = []

                # Text formats begin with '~~~'
                list_mixed_items = list_contents[0]
                for item in list_mixed_items:
                    if item.startswith('~~~'):
                        cell_format = item.split('~~~')[1]
                        item = self.dict_cell_format[cell_format]

                        list_items.append(item)
                    else:
                        list_items.append(item)

                worksheet.write_rich_string(cell, *list_items)

            else:
                text, cell_format = list_contents
                worksheet.write(cell, text, self.dict_cell_format[cell_format])

write_text_content

write_text_content(worksheet, input_text=None, txt_path=None, loc=None, retCellRange=None)

Write text content line by line to the worksheet.

Every line becomes one row, starting at loc (or the cursor), in a single column. Inline markup in a line:

  • {FORMAT_NAME} styles the text that follows it with a format of dict_cell_format ({B}, {I}, {HEADER_2}, {BLUE_H2}, ...). Several signs in one line give a rich string with mixed formats (text before the first sign is allowed then). A line with exactly one sign must start with it, otherwise TypeError.
  • " [>] " (with the blanks) splits a line into cells that are written side by side in the same row.
  • a trailing [[FORMAT_NAME]] formats the whole cell (as a conditional format).

Parameters:

Name Type Description Default
worksheet Worksheet

Worksheet to write on.

required
input_text str or None

Text to write, lines separated by "\n".

None
txt_path str or None

Path of a text file whose lines are written. Give exactly one of input_text and txt_path.

None
loc tuple of int or None

(row, col) (zero-based) of the first line. None starts at the cursor.

None
retCellRange str or None

"text" returns the range in A1 notation; "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0. The range covers only the first column, and its last row is one past the last line.

None

Returns:

Type Description
int or str or list of int

0, or the range as selected by retCellRange.

Raises:

Type Description
ValueError

If both or neither of input_text and txt_path are given.

Notes

The cursor is set to the last row written (curr_row = start_row + number_of_lines - 1) and curr_col is not changed, even when loc is given. End input_text with "\n" so that the next block starts below the text; otherwise it overwrites the last line. The file given by txt_path is opened and not closed explicitly.

Source code in ExcelMaster/ExcelMaster.py
def write_text_content(self, worksheet, input_text = None, txt_path = None, loc = None, retCellRange = None):
    r"""Write text content line by line to the worksheet.

    Every line becomes one row, starting at ``loc`` (or the cursor), in a single column. Inline markup in a line:

    * ``{FORMAT_NAME}`` styles the text that follows it with a format of ``dict_cell_format`` (``{B}``, ``{I}``,
      ``{HEADER_2}``, ``{BLUE_H2}``, ...). Several signs in one line give a rich string with mixed formats (text before
      the first sign is allowed then). A line with exactly one sign must start with it, otherwise ``TypeError``.
    * ``" [>] "`` (with the blanks) splits a line into cells that are written side by side in the same row.
    * a trailing ``[[FORMAT_NAME]]`` formats the whole cell (as a conditional format).

    Parameters
    ----------
    worksheet : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    input_text : str or None, default None
        Text to write, lines separated by ``"\n"``.
    txt_path : str or None, default None
        Path of a text file whose lines are written. Give exactly one of ``input_text`` and ``txt_path``.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the first line. None starts at the cursor.
    retCellRange : str or None, default None
        ``"text"`` returns the range in A1 notation; ``"value"`` returns ``[first_row, first_col, last_row, last_col]``
        (zero-based); None returns 0. The range covers only the first column, and its last row is one past the last line.

    Returns
    -------
    int or str or list of int
        0, or the range as selected by ``retCellRange``.

    Raises
    ------
    ValueError
        If both or neither of ``input_text`` and ``txt_path`` are given.

    Notes
    -----
    The cursor is set to the last row written (``curr_row = start_row + number_of_lines - 1``) and ``curr_col`` is not
    changed, even when ``loc`` is given. End ``input_text`` with ``"\n"`` so that the next block starts below the text;
    otherwise it overwrites the last line. The file given by ``txt_path`` is opened and not closed explicitly.
    """

    row_anchor = loc[0] if loc else self.curr_row
    col_anchor = loc[1] if loc else self.curr_col

    start_row = row_anchor
    start_col = col_anchor

    if (input_text) and (txt_path):
        raise ValueError("Please give either input_text or txt_path, not both !!!")

    if (input_text is None) and (txt_path is None):
        raise ValueError("Please specify either input_text or txt_path.")

    if txt_path is not None:
        file_notes = open(txt_path, 'r')
        textlines = file_notes.readlines()

    if input_text is not None:
        textlines = input_text.split("\n")

    for line in textlines:
        # Write text line by line
        loc = xl_rowcol_to_cell(row_anchor, col_anchor)

        if " [>] " in line:
            line_split = line.split(" [>] ")
            col_shift = 0
            for sub_line in line_split:
                sub_line = sub_line.rstrip()
                self.__parse_line_by_format_sign(worksheet = worksheet, loc = loc, line = sub_line)
                col_shift += 1
                loc = xl_rowcol_to_cell(row_anchor, col_anchor + col_shift)
        else:
            self.__parse_line_by_format_sign(worksheet = worksheet, loc = loc, line = line)

        row_anchor += 1

    self.curr_row = (row_anchor - 1)

    end_row = row_anchor
    end_col = col_anchor

    written_range = [start_row, start_col, end_row, end_col]

    if self.verbose:
        logging.info(f"Text Written in Cell Range: {self.to_cell_range_text(*written_range)}")

    if retCellRange == "text":
        return self.to_cell_range_text(*written_range)
    if retCellRange == "value":
        return written_range

    return 0

plot_boxplot staticmethod

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)

Plot Box Plot Chart using matplotlib.

Draws one box per distinct value of x (in ascending order) from the values of y. Both axis labels are reformatted with string_proc ("credit_score" becomes "Credit Score"). The figure is closed before the function returns.

Parameters:

Name Type Description Default
df DataFrame

Source data.

required
x str

Column that defines the groups, one box per distinct value.

required
y str

Numeric column whose distribution is drawn in each box.

required
y_percentage bool

If True, multiply the values of y by 100 and format the y axis as percent (PercentFormatter(100)).

False
colored_box bool

If True, fill the boxes with the colors of color_grp (alpha 0.7); otherwise draw unfilled boxes.

True
color_grp tuple or str or list of str

Box colors, as accepted by color_input_validation: a (start_num, step) tuple that picks XKCD named colors, one color code for all boxes, or a list with one color code per box. Used only when colored_box is True.

(10, 1)
title str

Title of the plot.

""
fontsize int

Font size of the axis labels and ticks (the title is fontsize + 4).

14
figsize tuple of float

Figure size (width, height) in inches (the figure is created with 200 dpi).

(8, 6)
show_fig bool

If True, display the figure with plt.show().

True
img_path str or None

If given, save the figure to this path (200 dpi).

None
transp_bg bool

If True, save the image with a transparent background.

False

Returns:

Type Description
tuple

(fig, ax): the matplotlib Figure and Axes.

Notes

y_percentage=True multiplies the values of y by 100 and labels the axis as percent, so it is meant for rates between 0 and 1. With y_percentage=False the values are drawn as they are.

Source code in ExcelMaster/ExcelMaster.py
@staticmethod
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):
    """Plot Box Plot Chart using matplotlib.

    Draws one box per distinct value of ``x`` (in ascending order) from the values of ``y``. Both axis labels are
    reformatted with ``string_proc`` (``"credit_score"`` becomes ``"Credit Score"``). The figure is closed before the
    function returns.

    Parameters
    ----------
    df : pandas.DataFrame
        Source data.
    x : str
        Column that defines the groups, one box per distinct value.
    y : str
        Numeric column whose distribution is drawn in each box.
    y_percentage : bool, default False
        If True, multiply the values of ``y`` by 100 and format the y axis as percent (``PercentFormatter(100)``).
    colored_box : bool, default True
        If True, fill the boxes with the colors of ``color_grp`` (alpha 0.7); otherwise draw unfilled boxes.
    color_grp : tuple or str or list of str, default (10, 1)
        Box colors, as accepted by ``color_input_validation``: a ``(start_num, step)`` tuple that picks XKCD named colors, one
        color code for all boxes, or a list with one color code per box. Used only when ``colored_box`` is True.
    title : str, default ""
        Title of the plot.
    fontsize : int, default 14
        Font size of the axis labels and ticks (the title is ``fontsize + 4``).
    figsize : tuple of float, default (8, 6)
        Figure size ``(width, height)`` in inches (the figure is created with 200 dpi).
    show_fig : bool, default True
        If True, display the figure with ``plt.show()``.
    img_path : str or None, default None
        If given, save the figure to this path (200 dpi).
    transp_bg : bool, default False
        If True, save the image with a transparent background.

    Returns
    -------
    tuple
        ``(fig, ax)``: the matplotlib ``Figure`` and ``Axes``.

    Notes
    -----
    ``y_percentage=True`` multiplies the values of ``y`` by 100 and labels the axis as percent, so it is meant for rates
    between 0 and 1. With ``y_percentage=False`` the values are drawn as they are.
    """

    plt.style.use('default')

    fig, ax = plt.subplots(1, 1, figsize = figsize, dpi=200)

    box_plot_data = convert_to_boxplot_data(df, x, y, y_percentage)

    bplot = ax.boxplot(box_plot_data.values(), patch_artist=colored_box, widths = 0.4)
    ax.set_xticklabels(box_plot_data.keys(), fontsize = fontsize)
    ax.set_ylabel(string_proc(y), color='black', fontsize = fontsize)
    ax.set_xlabel(string_proc(x), color='black', fontsize = fontsize)
    ax.tick_params(axis='both', labelsize=fontsize)
    ax.grid(True, 'major', 'y', ls='--', lw=.5, c='k', alpha=.3)
    ax.set_title(title, fontsize=fontsize + 4)

    if colored_box:
        colors = color_input_validation(color_grp, len(box_plot_data.keys()))
        for patch, color in zip(bplot['boxes'], colors):
            patch.set_facecolor(color)
            patch.set_alpha(.7)

    if y_percentage:
        plt.gca().yaxis.set_major_formatter(PercentFormatter(100)) 
    plt.tight_layout()

    if show_fig:
        plt.show()

    if img_path:
        fig.savefig(img_path, transparent = transp_bg, dpi=200)

    plt.close()
    return fig, ax

write_boxplot

write_boxplot(ws, df, x, y, y_percentage=False, colored_box=True, color_grp=(10, 1), title='', fontsize=14, figsize=(30, 13), show_fig=False, img_path=None, transp_bg=False, loc=None, skipby='row', retCellRange=None)

Write boxplot to Worksheet.

Draws the box plot with plot_boxplot into a temporary PNG file (./.tmp_image_<date time><random number>.png in the working directory), resizes the file to figsize worksheet cells and inserts it with insert_image.

Parameters:

Name Type Description Default
ws Worksheet

Worksheet to write on (note the name: ws, not worksheet).

required
df DataFrame

Source data.

required
x str

Column that defines the groups, one box per distinct value.

required
y str

Numeric column whose distribution is drawn in each box.

required
y_percentage bool

If True, multiply the values by 100 and format the y axis as percent (see plot_boxplot).

False
colored_box bool

If True, fill the boxes with the colors of color_grp.

True
color_grp tuple or str or list of str

Box colors, as accepted by color_input_validation.

(10, 1)
title str

Title of the plot.

""
fontsize int

Font size of the axis labels and ticks.

14
figsize tuple of int

Size of the inserted image as (rows, columns) of worksheet cells, not inches (unlike plot_boxplot): the image file is resized to columns * default_col_width by rows * default_row_height pixels. It is not passed to the figure itself.

(30, 13)
show_fig bool

If True, display the figure with plt.show().

False
img_path str or None

Not used: the image is always saved to the temporary file.

None
transp_bg bool

If True, save the image with a transparent background.

False
loc tuple of int or None

(row, col) (zero-based) of the top-left cell of the image. None starts at the cursor.

None
skipby str or None

Cursor move after the insertion, as in insert_image.

"row"
retCellRange str or None

"text" returns the covered range in A1 notation; "value" returns [first_row, first_col, last_row, last_col] (zero-based); None returns 0.

None

Returns:

Type Description
int or str or list of int

The result of insert_image: 0, or the covered range as selected by retCellRange.

Notes

The temporary image stays in the working directory until close_workbook (or remove_tmp_img) deletes it.

Source code in ExcelMaster/ExcelMaster.py
def write_boxplot(self, ws, df, x, y, y_percentage = False, colored_box = True, color_grp = (10, 1), title = "", 
                  fontsize = 14, figsize = (30, 13), show_fig = False, img_path = None, transp_bg = False,
                  loc = None, skipby = "row", retCellRange = None):
    """Write boxplot to Worksheet.

    Draws the box plot with ``plot_boxplot`` into a temporary PNG file (``./.tmp_image_<date time><random number>.png`` in
    the working directory), resizes the file to ``figsize`` worksheet cells and inserts it with ``insert_image``.

    Parameters
    ----------
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on (note the name: ``ws``, not ``worksheet``).
    df : pandas.DataFrame
        Source data.
    x : str
        Column that defines the groups, one box per distinct value.
    y : str
        Numeric column whose distribution is drawn in each box.
    y_percentage : bool, default False
        If True, multiply the values by 100 and format the y axis as percent (see ``plot_boxplot``).
    colored_box : bool, default True
        If True, fill the boxes with the colors of ``color_grp``.
    color_grp : tuple or str or list of str, default (10, 1)
        Box colors, as accepted by ``color_input_validation``.
    title : str, default ""
        Title of the plot.
    fontsize : int, default 14
        Font size of the axis labels and ticks.
    figsize : tuple of int, default (30, 13)
        Size of the inserted image as ``(rows, columns)`` of worksheet cells, not inches (unlike ``plot_boxplot``): the
        image file is resized to ``columns * default_col_width`` by ``rows * default_row_height`` pixels. It is not passed to
        the figure itself.
    show_fig : bool, default False
        If True, display the figure with ``plt.show()``.
    img_path : str or None, default None
        Not used: the image is always saved to the temporary file.
    transp_bg : bool, default False
        If True, save the image with a transparent background.
    loc : tuple of int or None, default None
        ``(row, col)`` (zero-based) of the top-left cell of the image. None starts at the cursor.
    skipby : str or None, default "row"
        Cursor move after the insertion, as in ``insert_image``.
    retCellRange : str or None, default None
        ``"text"`` returns the covered range in A1 notation; ``"value"`` returns
        ``[first_row, first_col, last_row, last_col]`` (zero-based); None returns 0.

    Returns
    -------
    int or str or list of int
        The result of ``insert_image``: 0, or the covered range as selected by ``retCellRange``.

    Notes
    -----
    The temporary image stays in the working directory until ``close_workbook`` (or ``remove_tmp_img``) deletes it.
    """

    currTime = getCurrentDateTime()
    rn = str(random.randrange(0, 1000))
    rn_num = currTime + rn
    tmp_image_path = f"./.tmp_image_{rn_num}.png"

    ExcelMaster.plot_boxplot(df = df, 
                             x = x, y = y, 
                             y_percentage = y_percentage, 
                             colored_box = colored_box, 
                             color_grp = color_grp, 
                             title = title, 
                             fontsize = fontsize, 
                             show_fig = show_fig, 
                             transp_bg = transp_bg,
                             img_path = tmp_image_path)

    self._resize_image(tmp_image_path, figsize, tmp_image_path)
    ret_range = self.insert_image(ws, figPath=tmp_image_path, figScale=(1, 1), loc = loc, skipby = skipby, retCellRange = retCellRange)

    return ret_range

getStartDateofLatestWeek

getStartDateofLatestWeek(retStr=True)

Date of first day of last week.

Returns the Sunday that immediately precedes the Monday starting the current week (weeks start on Monday and are counted with %W). Every day from Monday to Sunday of one week gives the same result, for example 2026-09-27 for 2026-09-28 through 2026-10-04.

Parameters:

Name Type Description Default
retStr bool

If True, return the date as a "%Y-%m-%d" string; otherwise return a datetime.datetime at midnight.

True

Returns:

Type Description
str or datetime

The date described above.

Source code in ExcelMaster/Utility.py
def getStartDateofLatestWeek(retStr=True):
    """Date of first day of last week.

    Returns the Sunday that immediately precedes the Monday starting the current week (weeks start on Monday and are
    counted with ``%W``). Every day from Monday to Sunday of one week gives the same result, for example 2026-09-27 for
    2026-09-28 through 2026-10-04.

    Parameters
    ----------
    retStr : bool, default True
        If True, return the date as a ``"%Y-%m-%d"`` string; otherwise return a ``datetime.datetime`` at midnight.

    Returns
    -------
    str or datetime.datetime
        The date described above.
    """
    import datetime
    today = datetime.date.today()
    curr_wk = today.strftime("%W")
    d = f"{str(today.year)}-W{curr_wk}"
    r = datetime.datetime.strptime(d + '-1', "%Y-W%W-%w")
    res = (r + datetime.timedelta(days=-1))
    if retStr:
        return res.strftime("%Y-%m-%d")
    return res

getLastCompletedVintage

getLastCompletedVintage(start_date=None, format='%Y-%m-%d', vintage=False)

Last Completed Vintage

Returns the last day of the month that precedes the month of start_date (or of today), that is the most recent completed month.

Parameters:

Name Type Description Default
start_date str or None

Reference date as text in format. None uses today's date.

None
format str

strptime / strftime format used to parse start_date and to format the result.

"%Y-%m-%d"
vintage bool

If True, return the month as a YYYYMM integer (for example 202609) instead of the formatted date. The digits are cut from the first 7 characters of the formatted date, so this is only correct when format starts with %Y-%m (the default does).

False

Returns:

Type Description
str or int

The last day of the previous month formatted with format (for example "2026-09-30"), or the YYYYMM integer when vintage is True.

Source code in ExcelMaster/Utility.py
def getLastCompletedVintage(start_date = None, format="%Y-%m-%d", vintage=False):
    """Last Completed Vintage

    Returns the last day of the month that precedes the month of ``start_date`` (or of today), that is the most recent
    completed month.

    Parameters
    ----------
    start_date : str or None, default None
        Reference date as text in ``format``. None uses today's date.
    format : str, default "%Y-%m-%d"
        ``strptime`` / ``strftime`` format used to parse ``start_date`` and to format the result.
    vintage : bool, default False
        If True, return the month as a ``YYYYMM`` integer (for example ``202609``) instead of the formatted date. The digits
        are cut from the first 7 characters of the formatted date, so this is only correct when ``format`` starts with
        ``%Y-%m`` (the default does).

    Returns
    -------
    str or int
        The last day of the previous month formatted with ``format`` (for example ``"2026-09-30"``), or the ``YYYYMM``
        integer when ``vintage`` is True.
    """
    import datetime

    todayDate = datetime.date.today()

    if start_date is not None:
        todayDate = datetime.datetime.strptime(start_date, format).date()    

    lastM = todayDate.replace(day=1) - datetime.timedelta(days=1)
    if vintage:
        return int(lastM.strftime(format)[0:7].replace("-",""))
    return lastM.strftime(format)

vin2quar

vin2quar(strDate)

String Vintage to Quarter (if completed month).

Parameters:

Name Type Description Default
strDate str

Vintage as "YYYYMM" text; only the first six characters are read.

required

Returns:

Type Description
str

"YYYYQn" (for example "2026Q2" for "202606") when the month is the last month of a quarter (3, 6, 9 or 12); otherwise strDate unchanged.

Source code in ExcelMaster/Utility.py
def vin2quar(strDate):
    """String Vintage to Quarter (if completed month).

    Parameters
    ----------
    strDate : str
        Vintage as ``"YYYYMM"`` text; only the first six characters are read.

    Returns
    -------
    str
        ``"YYYYQn"`` (for example ``"2026Q2"`` for ``"202606"``) when the month is the last month of a quarter (3, 6, 9 or
        12); otherwise ``strDate`` unchanged.
    """
    year = int(strDate[:4])
    month = int(strDate[4:6])
    completed_q = [3, 6, 9, 12]
    if month in completed_q:
        q = (month-1)//3 + 1
        return str(year) + "Q" + str(q)
    return strDate

list_files

list_files(location, pattern)

List all files.

Parameters:

Name Type Description Default
location str

Directory that is searched recursively (os.walk).

required
pattern str

Regular expression searched (re.search) in each file name.

required

Returns:

Type Description
list of str

Bare file names (without their directory) of the matching files in walk order; a name that exists in several directories appears once per directory.

Source code in ExcelMaster/Utility.py
def list_files(location, pattern):
    """List all files.

    Parameters
    ----------
    location : str
        Directory that is searched recursively (``os.walk``).
    pattern : str
        Regular expression searched (``re.search``) in each file name.

    Returns
    -------
    list of str
        Bare file names (without their directory) of the matching files in walk order; a name that exists in several
        directories appears once per directory.
    """
    import re
    res = []
    for root, dirs, files in os.walk(location):
        for file in files:
            if re.search(pattern, file):
                 res.append(file)
    return res

getCurrentDateTime

getCurrentDateTime(fmt='%Y%m%d%H%M%S')

Get Current DateTime

Parameters:

Name Type Description Default
fmt str

strftime format of the result.

"%Y%m%d%H%M%S"

Returns:

Type Description
str

Current local date and time formatted with fmt.

Source code in ExcelMaster/Utility.py
def getCurrentDateTime(fmt = "%Y%m%d%H%M%S"):
    """Get Current DateTime

    Parameters
    ----------
    fmt : str, default "%Y%m%d%H%M%S"
        ``strftime`` format of the result.

    Returns
    -------
    str
        Current local date and time formatted with ``fmt``.
    """
    import datetime
    return datetime.datetime.now().strftime(fmt)

input_table_proc

input_table_proc(tbl)

Process Input Table.

Lower-cases the column names of a table.

Parameters:

Name Type Description Default
tbl DataFrame

Table with string column names.

required

Returns:

Type Description
DataFrame

The same object with lower-cased column names; the caller's DataFrame is renamed in place.

Source code in ExcelMaster/Utility.py
def input_table_proc(tbl):
    """Process Input Table.

    Lower-cases the column names of a table.

    Parameters
    ----------
    tbl : pandas.DataFrame
        Table with string column names.

    Returns
    -------
    pandas.DataFrame
        The same object with lower-cased column names; the caller's DataFrame is renamed in place.
    """
    tbl.columns = [x.lower() for x in tbl.columns]
    return tbl

get_file_extension

get_file_extension(input_path)

Get File Extentsion for a given file Path.

Parameters:

Name Type Description Default
input_path str

File path or name.

required

Returns:

Type Description
str

The extension including the leading dot, in its original case (".csv"), or "" when there is none.

Source code in ExcelMaster/Utility.py
def get_file_extension(input_path):
    """Get File Extentsion for a given file Path.

    Parameters
    ----------
    input_path : str
        File path or name.

    Returns
    -------
    str
        The extension including the leading dot, in its original case (``".csv"``), or ``""`` when there is none.
    """
    return os.path.splitext(input_path)[1]

input_validation

input_validation(x, sep=',')

Input Validation.

Loads a table from a file path, or accepts a DataFrame, and lower-cases its column names.

Parameters:

Name Type Description Default
x str or DataFrame

A DataFrame, or the path of a file: a .sas7bdat file (the extension is compared case-sensitively) is read with pd.read_sas(encoding="latin-1"), any other file is read as delimited text with pd.read_csv.

required
sep str

Field delimiter of the text file; ignored for .sas7bdat files and DataFrames.

","

Returns:

Type Description
DataFrame

The table with lower-cased column names. A DataFrame passed in is returned as the same object, so its columns are renamed in place.

Raises:

Type Description
AttributeError

If x is neither a string nor a DataFrame (the exception type is AttributeError, not TypeError).

Source code in ExcelMaster/Utility.py
def input_validation(x, sep=","):
    """Input Validation.

    Loads a table from a file path, or accepts a DataFrame, and lower-cases its column names.

    Parameters
    ----------
    x : str or pandas.DataFrame
        A DataFrame, or the path of a file: a ``.sas7bdat`` file (the extension is compared case-sensitively) is read
        with ``pd.read_sas(encoding="latin-1")``, any other file is read as delimited text with ``pd.read_csv``.
    sep : str, default ","
        Field delimiter of the text file; ignored for ``.sas7bdat`` files and DataFrames.

    Returns
    -------
    pandas.DataFrame
        The table with lower-cased column names. A DataFrame passed in is returned as the same object, so its columns
        are renamed in place.

    Raises
    ------
    AttributeError
        If ``x`` is neither a string nor a DataFrame (the exception type is AttributeError, not TypeError).
    """
    import os
    if isinstance(x, str):
        if get_file_extension(x) == ".sas7bdat":
            res = pd.read_sas(x, encoding="latin-1")
        else:
            res = pd.read_csv(x, sep = sep)
        res = input_table_proc(res)
        return res
    elif isinstance(x, pd.DataFrame):
        return input_table_proc(x)
    else:
        raise AttributeError("Only Support csv/sas7bdat Path or Panda DataFrame as Input!!!")

val_input_condition

val_input_condition(target, condition=('>', 20))

Condition Tuple Validation.

Parameters:

Name Type Description Default
target float or str

Value to test.

required
condition tuple or str

A string tests equality: target == condition. A (operator, value) tuple compares target with float(value); the operator (case-insensitive, surrounding blanks ignored) is one of ">" / "gt", "<" / "lt", "=" / "eq", ">=" / "gte", "<=" / "lte".

(">", 20)

Returns:

Type Description
bool or None

The result of the test. None (falsy) for an operator that is not in the list above, for example "==" or "!=".

Source code in ExcelMaster/Utility.py
def val_input_condition(target, condition = (">", 20)):
    """Condition Tuple Validation.

    Parameters
    ----------
    target : float or str
        Value to test.
    condition : tuple or str, default (">", 20)
        A string tests equality: ``target == condition``. A ``(operator, value)`` tuple compares ``target`` with
        ``float(value)``; the operator (case-insensitive, surrounding blanks ignored) is one of ``">"`` / ``"gt"``,
        ``"<"`` / ``"lt"``, ``"="`` / ``"eq"``, ``">="`` / ``"gte"``, ``"<="`` / ``"lte"``.

    Returns
    -------
    bool or None
        The result of the test. None (falsy) for an operator that is not in the list above, for example ``"=="`` or
        ``"!="``.
    """
    if isinstance(condition, str):
        return (target == condition)

    else:
        operator = condition[0].strip().lower()
        value = float(condition[1])

        if operator == '>' or operator == 'gt':
            return (target > value)
        elif operator == '<' or operator == 'lt':
            return (target < value)
        elif operator == '=' or operator == 'eq':
            return (target == value)
        elif operator == '>=' or operator == 'gte':
            return (target >= value)
        elif operator == '<=' or operator == 'lte':
            return (target <= value)
        elif operator == '=' or operator == 'eq':
            return (target == value)

get_quarter

get_quarter(strDate)

Get the quarter number of a vintage.

Parameters:

Name Type Description Default
strDate str

Vintage as "YYYYMM" text; characters 5 and 6 are read as the month.

required

Returns:

Type Description
int

Quarter of the month, 1 to 4 (for example 2 for "202605").

Source code in ExcelMaster/Utility.py
def get_quarter(strDate):
    """Get the quarter number of a vintage.

    Parameters
    ----------
    strDate : str
        Vintage as ``"YYYYMM"`` text; characters 5 and 6 are read as the month.

    Returns
    -------
    int
        Quarter of the month, 1 to 4 (for example ``2`` for ``"202605"``).
    """
    return ((int(strDate[4:6])-1)//3) + 1

tanspose_dataframe

tanspose_dataframe(df, index_col)

Transpose Pandas DataFrame.

The spelling of the function name is historical.

Parameters:

Name Type Description Default
df DataFrame

Table to transpose.

required
index_col str or list of str

Column(s) set as the index before the transposition; their values become the column labels of the result.

required

Returns:

Type Description
DataFrame

df.set_index(index_col).T.reset_index(): the former column names are in a leading column named index.

Source code in ExcelMaster/Utility.py
def tanspose_dataframe(df, index_col):
    """Transpose Pandas DataFrame.

    The spelling of the function name is historical.

    Parameters
    ----------
    df : pandas.DataFrame
        Table to transpose.
    index_col : str or list of str
        Column(s) set as the index before the transposition; their values become the column labels of the result.

    Returns
    -------
    pandas.DataFrame
        ``df.set_index(index_col).T.reset_index()``: the former column names are in a leading column named ``index``.
    """
    df = df.set_index(index_col).T.reset_index()
    return df

convert_perc_str_to_float

convert_perc_str_to_float(df, cols)

Percentage to Float.

Parameters:

Name Type Description Default
df DataFrame

Table to convert.

required
cols list of str

Columns to convert. A column is converted only when its dtype is object (percentage text such as "12.5%"): the trailing % is removed and the number is divided by 100. Columns of any other dtype are left unchanged.

required

Returns:

Type Description
DataFrame

df itself; the columns are replaced in place.

Source code in ExcelMaster/Utility.py
def convert_perc_str_to_float(df, cols):
    """Percentage to Float.

    Parameters
    ----------
    df : pandas.DataFrame
        Table to convert.
    cols : list of str
        Columns to convert. A column is converted only when its dtype is ``object`` (percentage text such as
        ``"12.5%"``): the trailing ``%`` is removed and the number is divided by 100. Columns of any other dtype are left
        unchanged.

    Returns
    -------
    pandas.DataFrame
        ``df`` itself; the columns are replaced in place.
    """
    for col in cols:
        if str(df[col].dtypes) == 'object':
            df[col] = df[col].str.rstrip('%').astype('float') / 100
    return df

color_hex2rgb

color_hex2rgb(hex_code)

Convert Color Hex Code to RGB Tuple.

Parameters:

Name Type Description Default
hex_code str

Color as six hexadecimal digits with an optional leading # ("#FF8800" or "ff8800"). The three-digit shorthand is not supported.

required

Returns:

Type Description
tuple of int

(red, green, blue) with values from 0 to 255, for example (255, 136, 0).

Source code in ExcelMaster/Utility.py
def color_hex2rgb(hex_code):
    """Convert Color Hex Code to RGB Tuple.

    Parameters
    ----------
    hex_code : str
        Color as six hexadecimal digits with an optional leading ``#`` (``"#FF8800"`` or ``"ff8800"``). The three-digit
        shorthand is not supported.

    Returns
    -------
    tuple of int
        ``(red, green, blue)`` with values from 0 to 255, for example ``(255, 136, 0)``.
    """
    hex_code = hex_code.lower()
    h = hex_code.lstrip('#')
    return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))

get_color_set

get_color_set(n, start_num=0, step=1, retName=False, lookupName=None)

Return a set of color code without replacement.

Picks colors from the XKCD color table of matplotlib (949 named colors).

Parameters:

Name Type Description Default
n int

Number of colors.

required
start_num int

Index in the XKCD table of the first color.

0
step int

Distance between two consecutive picked indices (start_num, start_num + step, ...). An index beyond the end of the table raises IndexError.

1
retName bool

If True, return a dict {color name: hex code} instead of a list of hex codes.

False
lookupName str or None

Regular expression that replaces the index-based picks: the first n colors whose table key matches it (re.search) are returned, fewer if fewer match. The key carries the "xkcd:" prefix ("xkcd:blue"), so the prefix is part of the searched text. start_num and step are then not used, but the index-based loop still runs first and can raise IndexError.

None

Returns:

Type Description
list of str or dict

Hex color codes ("#acc2d9"), or {name: code} when retName is True.

Source code in ExcelMaster/Utility.py
def get_color_set(n, start_num = 0, step = 1, retName = False, lookupName = None):
    """Return a set of color code without replacement.

    Picks colors from the XKCD color table of matplotlib (949 named colors).

    Parameters
    ----------
    n : int
        Number of colors.
    start_num : int, default 0
        Index in the XKCD table of the first color.
    step : int, default 1
        Distance between two consecutive picked indices (``start_num``, ``start_num + step``, ...). An index beyond the
        end of the table raises ``IndexError``.
    retName : bool, default False
        If True, return a dict ``{color name: hex code}`` instead of a list of hex codes.
    lookupName : str or None, default None
        Regular expression that replaces the index-based picks: the first ``n`` colors whose table key matches it
        (``re.search``) are returned, fewer if fewer match. The key carries the ``"xkcd:"`` prefix (``"xkcd:blue"``), so
        the prefix is part of the searched text. ``start_num`` and ``step`` are then not used, but the index-based loop still
        runs first and can raise ``IndexError``.

    Returns
    -------
    list of str or dict
        Hex color codes (``"#acc2d9"``), or ``{name: code}`` when ``retName`` is True.
    """
    import matplotlib.colors as mcolors
    import re

    colors = list(mcolors.XKCD_COLORS.items())
    color_set = {}

    for i in range(start_num, start_num + n * step, step):
        name = colors[i][0].replace("xkcd:", "")
        color = colors[i][1]
        color_set[name] = color

    if lookupName:
        color_set = {}
        i = 0
        for name, color in colors:
            if len(color_set.items()) == n:
                break
            if re.search(lookupName, name):
                color_set[name.replace("xkcd:", "")] = color
            i += 1

    if retName:
        return color_set
    return list(color_set.values())

string_proc

string_proc(x)

Process a given String.

Parameters:

Name Type Description Default
x str

Text whose words are separated by underscores, for example a column name.

required

Returns:

Type Description
str

The words capitalized (first letter upper-case, the rest lower-case) and joined by single spaces, for example "credit_SCORE_v2" becomes "Credit Score V2".

Source code in ExcelMaster/Utility.py
def string_proc(x):
    """Process a given String.

    Parameters
    ----------
    x : str
        Text whose words are separated by underscores, for example a column name.

    Returns
    -------
    str
        The words capitalized (first letter upper-case, the rest lower-case) and joined by single spaces, for example
        ``"credit_SCORE_v2"`` becomes ``"Credit Score V2"``.
    """
    x_list = x.split("_")
    x_list = [x.strip().capitalize() for x in x_list]
    x = " ".join(x_list)
    return x

convert_to_boxplot_data

convert_to_boxplot_data(df, x, y, y_percentage=False)

Convert dataframe to boxplot data.

Parameters:

Name Type Description Default
df DataFrame

Source table.

required
x str

Column that defines the groups (one box per distinct value).

required
y str

Column with the values of each group.

required
y_percentage bool

If True, multiply every value by 100.

False

Returns:

Type Description
dict

{group value: list of y values}, with the groups in ascending order of x.

Source code in ExcelMaster/Utility.py
def convert_to_boxplot_data(df, x, y, y_percentage = False):
    """Convert dataframe to boxplot data.

    Parameters
    ----------
    df : pandas.DataFrame
        Source table.
    x : str
        Column that defines the groups (one box per distinct value).
    y : str
        Column with the values of each group.
    y_percentage : bool, default False
        If True, multiply every value by 100.

    Returns
    -------
    dict
        ``{group value: list of y values}``, with the groups in ascending order of ``x``.
    """
    x_unique_value = df[x].sort_values().unique().tolist()

    box_plot_data = {}
    for v in x_unique_value:
        box_plot_data[v] = [num * 100 if y_percentage else num for num in df[df[x] == v][y].tolist()]
    return box_plot_data

color_input_validation

color_input_validation(color_grp, val_n)

Perform Color Input Validation.

Parameters:

Name Type Description Default
color_grp tuple or str or list of str

A 2-tuple (start_num, step) picks val_n named colors from the XKCD table with get_color_set; a string is one color code used for all boxes; a list of strings is used as it is and must have exactly val_n entries.

required
val_n int

Number of colors needed (number of boxes).

required

Returns:

Type Description
list of str

Color codes, one per box.

Raises:

Type Description
ValueError

If color_grp has none of the three accepted forms (this includes a list whose length is not val_n).

Source code in ExcelMaster/Utility.py
def color_input_validation(color_grp, val_n):
    """Perform Color Input Validation.

    Parameters
    ----------
    color_grp : tuple or str or list of str
        A 2-tuple ``(start_num, step)`` picks ``val_n`` named colors from the XKCD table with ``get_color_set``; a string is
        one color code used for all boxes; a list of strings is used as it is and must have exactly ``val_n`` entries.
    val_n : int
        Number of colors needed (number of boxes).

    Returns
    -------
    list of str
        Color codes, one per box.

    Raises
    ------
    ValueError
        If ``color_grp`` has none of the three accepted forms (this includes a list whose length is not ``val_n``).
    """
    if isinstance(color_grp, tuple) and len(color_grp) == 2:
        # Customize colors
        cols = get_color_set(val_n, color_grp[0], color_grp[1], False)
        colors = cols
    elif isinstance(color_grp, str):
        colors = [color_grp] * val_n
    elif (isinstance(color_grp, list)) and (all([isinstance(x, str) for x in color_grp])) and (len(color_grp) == val_n):
        colors = color_grp
    else:
        raise ValueError("Please give valid color_grp: tuple of two numbers, list of color code or a single color code.")
    return colors

get_metric_shift

get_metric_shift(data, metric_name, nvars_col='nvars')

Calculate Metric Shift for Variable Reduction.

For every row, the slope between consecutive rows: (metric - metric of the previous row) / (nvars - nvars of the previous row), that is the change of the metric per variable added (or removed, when the rows are ordered by decreasing number of variables).

Parameters:

Name Type Description Default
data DataFrame

Table with one row per model, ordered by the number of variables.

required
metric_name str

Column holding the metric.

required
nvars_col str

Column holding the number of variables.

"nvars"

Returns:

Type Description
Series

The shift of every row, with the index of data. The first row is 0 (missing values are filled with 0). Rows whose number of variables equals that of the previous row divide by zero: inf / -inf, or 0 when the metric is unchanged too.

Source code in ExcelMaster/Utility.py
def get_metric_shift(data, metric_name, nvars_col = "nvars"):
    """Calculate Metric Shift for Variable Reduction.

    For every row, the slope between consecutive rows: ``(metric - metric of the previous row) / (nvars - nvars of the
    previous row)``, that is the change of the metric per variable added (or removed, when the rows are ordered by
    decreasing number of variables).

    Parameters
    ----------
    data : pandas.DataFrame
        Table with one row per model, ordered by the number of variables.
    metric_name : str
        Column holding the metric.
    nvars_col : str, default "nvars"
        Column holding the number of variables.

    Returns
    -------
    pandas.Series
        The shift of every row, with the index of ``data``. The first row is 0 (missing values are filled with 0). Rows
        whose number of variables equals that of the previous row divide by zero: ``inf`` / ``-inf``, or 0 when the metric
        is unchanged too.
    """
    metric_lift = (data[metric_name] - data[metric_name].shift(1))
    nvars_reduced = (data[nvars_col] - data[nvars_col].shift(1))
    return (metric_lift / nvars_reduced).fillna(0)

get_metrics_shift

get_metrics_shift(data, metric_cols)

Get Shift for List of Metrics.

Adds a column <metric>_shift for every metric, computed with get_metric_shift and its default nvars_col="nvars", so data needs a column named nvars.

Parameters:

Name Type Description Default
data DataFrame

Table with one row per model, ordered by the number of variables.

required
metric_cols list of str

Metric columns.

required

Returns:

Type Description
DataFrame

data itself with the added *_shift columns (modified in place).

Source code in ExcelMaster/Utility.py
def get_metrics_shift(data, metric_cols):
    """Get Shift for List of Metrics.

    Adds a column ``<metric>_shift`` for every metric, computed with ``get_metric_shift`` and its default
    ``nvars_col="nvars"``, so ``data`` needs a column named ``nvars``.

    Parameters
    ----------
    data : pandas.DataFrame
        Table with one row per model, ordered by the number of variables.
    metric_cols : list of str
        Metric columns.

    Returns
    -------
    pandas.DataFrame
        ``data`` itself with the added ``*_shift`` columns (modified in place).
    """
    for metric in metric_cols:
        if metric.startswith(tuple(metric_cols)):
            data[metric+"_shift"] = get_metric_shift(data, metric)
    return data

compute_overfitting_shift

compute_overfitting_shift(data, sample_prefix)

Calculate Overfitting Performance Shift.

Parameters:

Name Type Description Default
data DataFrame

Table with the metric columns of both samples, named <prefix><metric>.

required
sample_prefix tuple of str

(base_prefix, other_prefix). The columns whose names start with sample_prefix[0] and those that start with sample_prefix[1] are paired in column order (not by name).

required

Returns:

Type Description
DataFrame

data itself (modified in place) with one added column <other column>_shift per pair, equal to other / base - 1: the relative change of the second sample versus the first.

Raises:

Type Description
ValueError

If the two prefixes select a different number of columns.

Source code in ExcelMaster/Utility.py
def compute_overfitting_shift(data, sample_prefix):
    """Calculate Overfitting Performance Shift.

    Parameters
    ----------
    data : pandas.DataFrame
        Table with the metric columns of both samples, named ``<prefix><metric>``.
    sample_prefix : tuple of str
        ``(base_prefix, other_prefix)``. The columns whose names start with ``sample_prefix[0]`` and those that start with
        ``sample_prefix[1]`` are paired in column order (not by name).

    Returns
    -------
    pandas.DataFrame
        ``data`` itself (modified in place) with one added column ``<other column>_shift`` per pair, equal to
        ``other / base - 1``: the relative change of the second sample versus the first.

    Raises
    ------
    ValueError
        If the two prefixes select a different number of columns.
    """
    b_metrics = [x for x in data.columns if x.startswith(sample_prefix[0])]
    o_metrics = [x for x in data.columns if x.startswith(sample_prefix[1])]

    if len(b_metrics) == len(o_metrics):
        for b_metric, o_metric in zip(b_metrics, o_metrics):
            data[o_metric+"_shift"] = data[o_metric].div(data[b_metric]) - 1
        return data

    raise ValueError("The lengths of metrics between two samples are not the same in the given dataset!")

proc_psi_raw_report

proc_psi_raw_report(psi_raw_table, psi_title, keep_list=None, varname='variable', upper=True)

Processing Raw PSI Report generated from Takecopter.

Parameters:

Name Type Description Default
psi_raw_table str or DataFrame

Raw PSI table: a DataFrame or the path of a csv / sas7bdat file, read with input_validation (so the column names are lower-cased). It must contain the column var_for_psi.

required
psi_title str

Title placed on the top level of the two-level column header of the result.

required
keep_list list of str or None

Columns to keep, spelled as in the processed table (upper-case when upper is True). None or an empty list keeps all columns.

None
varname str

New name of the var_for_psi column, which becomes the index.

"variable"
upper bool

If True, upper-case the names of the remaining columns.

True

Returns:

Type Description
DataFrame

Table indexed by varname whose columns are the two-level pairs (psi_title, column name).

Source code in ExcelMaster/Utility.py
def proc_psi_raw_report(psi_raw_table, psi_title, keep_list = None, varname = "variable", upper=True):
    """Processing Raw PSI Report generated from Takecopter.

    Parameters
    ----------
    psi_raw_table : str or pandas.DataFrame
        Raw PSI table: a DataFrame or the path of a csv / sas7bdat file, read with ``input_validation`` (so the column
        names are lower-cased). It must contain the column ``var_for_psi``.
    psi_title : str
        Title placed on the top level of the two-level column header of the result.
    keep_list : list of str or None, default None
        Columns to keep, spelled as in the processed table (upper-case when ``upper`` is True). None or an empty list keeps
        all columns.
    varname : str, default "variable"
        New name of the ``var_for_psi`` column, which becomes the index.
    upper : bool, default True
        If True, upper-case the names of the remaining columns.

    Returns
    -------
    pandas.DataFrame
        Table indexed by ``varname`` whose columns are the two-level pairs ``(psi_title, column name)``.
    """
    psi_table = input_validation(psi_raw_table)
    psi_table = psi_table.rename(columns={"var_for_psi":varname})
    psi_table = psi_table.set_index(varname)
    if upper:
        psi_table.columns = [x.upper() for x in psi_table.columns]
    psi_table.columns = [[psi_title]*len(psi_table.columns),psi_table.columns]
    if keep_list:
        psi_table = psi_table[[(psi_title, x) for x in keep_list]]
    return psi_table

get_mean_risk

get_mean_risk(bivar_single_attr, value_range_col=['min_indep', 'max_indep'], dep_col='dep')

get average risk

Parameters:

Name Type Description Default
bivar_single_attr DataFrame

Bivariate table of one attribute with one row per bin.

required
value_range_col list of str

Columns with the bounds of each bin. A row whose bounds are all missing is the missing-value bin and is left out of mean_no_nan.

['min_indep', 'max_indep']
dep_col str

Column with the bad rate of each bin.

"dep"

Returns:

Type Description
DataFrame

bivar_single_attr itself with two constant columns added in place: mean (mean of dep_col over all rows) and mean_no_nan (mean over the rows that have at least one bin bound). Both are plain averages of the per-bin values, not weighted by bin size.

Source code in ExcelMaster/Utility.py
def get_mean_risk(bivar_single_attr, value_range_col = ['min_indep', 'max_indep'], dep_col = "dep"):
    """get average risk

    Parameters
    ----------
    bivar_single_attr : pandas.DataFrame
        Bivariate table of one attribute with one row per bin.
    value_range_col : list of str, default ['min_indep', 'max_indep']
        Columns with the bounds of each bin. A row whose bounds are all missing is the missing-value bin and is left out of
        ``mean_no_nan``.
    dep_col : str, default "dep"
        Column with the bad rate of each bin.

    Returns
    -------
    pandas.DataFrame
        ``bivar_single_attr`` itself with two constant columns added in place: ``mean`` (mean of ``dep_col`` over all rows)
        and ``mean_no_nan`` (mean over the rows that have at least one bin bound). Both are plain averages of the per-bin
        values, not weighted by bin size.
    """
    mean_wo_nan = bivar_single_attr.dropna(how = "all", subset=value_range_col)[dep_col].mean()
    mean_w_na = bivar_single_attr[dep_col].mean()
    bivar_single_attr["mean"] = mean_w_na
    bivar_single_attr["mean_no_nan"] = mean_wo_nan
    return bivar_single_attr

Report Templates: Template

Template

getStartDateofLatestWeek

getStartDateofLatestWeek(retStr=True)

Date of first day of last week.

Returns the Sunday that immediately precedes the Monday starting the current week (weeks start on Monday and are counted with %W). Every day from Monday to Sunday of one week gives the same result, for example 2026-09-27 for 2026-09-28 through 2026-10-04.

Parameters:

Name Type Description Default
retStr bool

If True, return the date as a "%Y-%m-%d" string; otherwise return a datetime.datetime at midnight.

True

Returns:

Type Description
str or datetime

The date described above.

Source code in ExcelMaster/Utility.py
def getStartDateofLatestWeek(retStr=True):
    """Date of first day of last week.

    Returns the Sunday that immediately precedes the Monday starting the current week (weeks start on Monday and are
    counted with ``%W``). Every day from Monday to Sunday of one week gives the same result, for example 2026-09-27 for
    2026-09-28 through 2026-10-04.

    Parameters
    ----------
    retStr : bool, default True
        If True, return the date as a ``"%Y-%m-%d"`` string; otherwise return a ``datetime.datetime`` at midnight.

    Returns
    -------
    str or datetime.datetime
        The date described above.
    """
    import datetime
    today = datetime.date.today()
    curr_wk = today.strftime("%W")
    d = f"{str(today.year)}-W{curr_wk}"
    r = datetime.datetime.strptime(d + '-1', "%Y-W%W-%w")
    res = (r + datetime.timedelta(days=-1))
    if retStr:
        return res.strftime("%Y-%m-%d")
    return res

getLastCompletedVintage

getLastCompletedVintage(start_date=None, format='%Y-%m-%d', vintage=False)

Last Completed Vintage

Returns the last day of the month that precedes the month of start_date (or of today), that is the most recent completed month.

Parameters:

Name Type Description Default
start_date str or None

Reference date as text in format. None uses today's date.

None
format str

strptime / strftime format used to parse start_date and to format the result.

"%Y-%m-%d"
vintage bool

If True, return the month as a YYYYMM integer (for example 202609) instead of the formatted date. The digits are cut from the first 7 characters of the formatted date, so this is only correct when format starts with %Y-%m (the default does).

False

Returns:

Type Description
str or int

The last day of the previous month formatted with format (for example "2026-09-30"), or the YYYYMM integer when vintage is True.

Source code in ExcelMaster/Utility.py
def getLastCompletedVintage(start_date = None, format="%Y-%m-%d", vintage=False):
    """Last Completed Vintage

    Returns the last day of the month that precedes the month of ``start_date`` (or of today), that is the most recent
    completed month.

    Parameters
    ----------
    start_date : str or None, default None
        Reference date as text in ``format``. None uses today's date.
    format : str, default "%Y-%m-%d"
        ``strptime`` / ``strftime`` format used to parse ``start_date`` and to format the result.
    vintage : bool, default False
        If True, return the month as a ``YYYYMM`` integer (for example ``202609``) instead of the formatted date. The digits
        are cut from the first 7 characters of the formatted date, so this is only correct when ``format`` starts with
        ``%Y-%m`` (the default does).

    Returns
    -------
    str or int
        The last day of the previous month formatted with ``format`` (for example ``"2026-09-30"``), or the ``YYYYMM``
        integer when ``vintage`` is True.
    """
    import datetime

    todayDate = datetime.date.today()

    if start_date is not None:
        todayDate = datetime.datetime.strptime(start_date, format).date()    

    lastM = todayDate.replace(day=1) - datetime.timedelta(days=1)
    if vintage:
        return int(lastM.strftime(format)[0:7].replace("-",""))
    return lastM.strftime(format)

vin2quar

vin2quar(strDate)

String Vintage to Quarter (if completed month).

Parameters:

Name Type Description Default
strDate str

Vintage as "YYYYMM" text; only the first six characters are read.

required

Returns:

Type Description
str

"YYYYQn" (for example "2026Q2" for "202606") when the month is the last month of a quarter (3, 6, 9 or 12); otherwise strDate unchanged.

Source code in ExcelMaster/Utility.py
def vin2quar(strDate):
    """String Vintage to Quarter (if completed month).

    Parameters
    ----------
    strDate : str
        Vintage as ``"YYYYMM"`` text; only the first six characters are read.

    Returns
    -------
    str
        ``"YYYYQn"`` (for example ``"2026Q2"`` for ``"202606"``) when the month is the last month of a quarter (3, 6, 9 or
        12); otherwise ``strDate`` unchanged.
    """
    year = int(strDate[:4])
    month = int(strDate[4:6])
    completed_q = [3, 6, 9, 12]
    if month in completed_q:
        q = (month-1)//3 + 1
        return str(year) + "Q" + str(q)
    return strDate

list_files

list_files(location, pattern)

List all files.

Parameters:

Name Type Description Default
location str

Directory that is searched recursively (os.walk).

required
pattern str

Regular expression searched (re.search) in each file name.

required

Returns:

Type Description
list of str

Bare file names (without their directory) of the matching files in walk order; a name that exists in several directories appears once per directory.

Source code in ExcelMaster/Utility.py
def list_files(location, pattern):
    """List all files.

    Parameters
    ----------
    location : str
        Directory that is searched recursively (``os.walk``).
    pattern : str
        Regular expression searched (``re.search``) in each file name.

    Returns
    -------
    list of str
        Bare file names (without their directory) of the matching files in walk order; a name that exists in several
        directories appears once per directory.
    """
    import re
    res = []
    for root, dirs, files in os.walk(location):
        for file in files:
            if re.search(pattern, file):
                 res.append(file)
    return res

getCurrentDateTime

getCurrentDateTime(fmt='%Y%m%d%H%M%S')

Get Current DateTime

Parameters:

Name Type Description Default
fmt str

strftime format of the result.

"%Y%m%d%H%M%S"

Returns:

Type Description
str

Current local date and time formatted with fmt.

Source code in ExcelMaster/Utility.py
def getCurrentDateTime(fmt = "%Y%m%d%H%M%S"):
    """Get Current DateTime

    Parameters
    ----------
    fmt : str, default "%Y%m%d%H%M%S"
        ``strftime`` format of the result.

    Returns
    -------
    str
        Current local date and time formatted with ``fmt``.
    """
    import datetime
    return datetime.datetime.now().strftime(fmt)

input_table_proc

input_table_proc(tbl)

Process Input Table.

Lower-cases the column names of a table.

Parameters:

Name Type Description Default
tbl DataFrame

Table with string column names.

required

Returns:

Type Description
DataFrame

The same object with lower-cased column names; the caller's DataFrame is renamed in place.

Source code in ExcelMaster/Utility.py
def input_table_proc(tbl):
    """Process Input Table.

    Lower-cases the column names of a table.

    Parameters
    ----------
    tbl : pandas.DataFrame
        Table with string column names.

    Returns
    -------
    pandas.DataFrame
        The same object with lower-cased column names; the caller's DataFrame is renamed in place.
    """
    tbl.columns = [x.lower() for x in tbl.columns]
    return tbl

get_file_extension

get_file_extension(input_path)

Get File Extentsion for a given file Path.

Parameters:

Name Type Description Default
input_path str

File path or name.

required

Returns:

Type Description
str

The extension including the leading dot, in its original case (".csv"), or "" when there is none.

Source code in ExcelMaster/Utility.py
def get_file_extension(input_path):
    """Get File Extentsion for a given file Path.

    Parameters
    ----------
    input_path : str
        File path or name.

    Returns
    -------
    str
        The extension including the leading dot, in its original case (``".csv"``), or ``""`` when there is none.
    """
    return os.path.splitext(input_path)[1]

input_validation

input_validation(x, sep=',')

Input Validation.

Loads a table from a file path, or accepts a DataFrame, and lower-cases its column names.

Parameters:

Name Type Description Default
x str or DataFrame

A DataFrame, or the path of a file: a .sas7bdat file (the extension is compared case-sensitively) is read with pd.read_sas(encoding="latin-1"), any other file is read as delimited text with pd.read_csv.

required
sep str

Field delimiter of the text file; ignored for .sas7bdat files and DataFrames.

","

Returns:

Type Description
DataFrame

The table with lower-cased column names. A DataFrame passed in is returned as the same object, so its columns are renamed in place.

Raises:

Type Description
AttributeError

If x is neither a string nor a DataFrame (the exception type is AttributeError, not TypeError).

Source code in ExcelMaster/Utility.py
def input_validation(x, sep=","):
    """Input Validation.

    Loads a table from a file path, or accepts a DataFrame, and lower-cases its column names.

    Parameters
    ----------
    x : str or pandas.DataFrame
        A DataFrame, or the path of a file: a ``.sas7bdat`` file (the extension is compared case-sensitively) is read
        with ``pd.read_sas(encoding="latin-1")``, any other file is read as delimited text with ``pd.read_csv``.
    sep : str, default ","
        Field delimiter of the text file; ignored for ``.sas7bdat`` files and DataFrames.

    Returns
    -------
    pandas.DataFrame
        The table with lower-cased column names. A DataFrame passed in is returned as the same object, so its columns
        are renamed in place.

    Raises
    ------
    AttributeError
        If ``x`` is neither a string nor a DataFrame (the exception type is AttributeError, not TypeError).
    """
    import os
    if isinstance(x, str):
        if get_file_extension(x) == ".sas7bdat":
            res = pd.read_sas(x, encoding="latin-1")
        else:
            res = pd.read_csv(x, sep = sep)
        res = input_table_proc(res)
        return res
    elif isinstance(x, pd.DataFrame):
        return input_table_proc(x)
    else:
        raise AttributeError("Only Support csv/sas7bdat Path or Panda DataFrame as Input!!!")

val_input_condition

val_input_condition(target, condition=('>', 20))

Condition Tuple Validation.

Parameters:

Name Type Description Default
target float or str

Value to test.

required
condition tuple or str

A string tests equality: target == condition. A (operator, value) tuple compares target with float(value); the operator (case-insensitive, surrounding blanks ignored) is one of ">" / "gt", "<" / "lt", "=" / "eq", ">=" / "gte", "<=" / "lte".

(">", 20)

Returns:

Type Description
bool or None

The result of the test. None (falsy) for an operator that is not in the list above, for example "==" or "!=".

Source code in ExcelMaster/Utility.py
def val_input_condition(target, condition = (">", 20)):
    """Condition Tuple Validation.

    Parameters
    ----------
    target : float or str
        Value to test.
    condition : tuple or str, default (">", 20)
        A string tests equality: ``target == condition``. A ``(operator, value)`` tuple compares ``target`` with
        ``float(value)``; the operator (case-insensitive, surrounding blanks ignored) is one of ``">"`` / ``"gt"``,
        ``"<"`` / ``"lt"``, ``"="`` / ``"eq"``, ``">="`` / ``"gte"``, ``"<="`` / ``"lte"``.

    Returns
    -------
    bool or None
        The result of the test. None (falsy) for an operator that is not in the list above, for example ``"=="`` or
        ``"!="``.
    """
    if isinstance(condition, str):
        return (target == condition)

    else:
        operator = condition[0].strip().lower()
        value = float(condition[1])

        if operator == '>' or operator == 'gt':
            return (target > value)
        elif operator == '<' or operator == 'lt':
            return (target < value)
        elif operator == '=' or operator == 'eq':
            return (target == value)
        elif operator == '>=' or operator == 'gte':
            return (target >= value)
        elif operator == '<=' or operator == 'lte':
            return (target <= value)
        elif operator == '=' or operator == 'eq':
            return (target == value)

get_quarter

get_quarter(strDate)

Get the quarter number of a vintage.

Parameters:

Name Type Description Default
strDate str

Vintage as "YYYYMM" text; characters 5 and 6 are read as the month.

required

Returns:

Type Description
int

Quarter of the month, 1 to 4 (for example 2 for "202605").

Source code in ExcelMaster/Utility.py
def get_quarter(strDate):
    """Get the quarter number of a vintage.

    Parameters
    ----------
    strDate : str
        Vintage as ``"YYYYMM"`` text; characters 5 and 6 are read as the month.

    Returns
    -------
    int
        Quarter of the month, 1 to 4 (for example ``2`` for ``"202605"``).
    """
    return ((int(strDate[4:6])-1)//3) + 1

tanspose_dataframe

tanspose_dataframe(df, index_col)

Transpose Pandas DataFrame.

The spelling of the function name is historical.

Parameters:

Name Type Description Default
df DataFrame

Table to transpose.

required
index_col str or list of str

Column(s) set as the index before the transposition; their values become the column labels of the result.

required

Returns:

Type Description
DataFrame

df.set_index(index_col).T.reset_index(): the former column names are in a leading column named index.

Source code in ExcelMaster/Utility.py
def tanspose_dataframe(df, index_col):
    """Transpose Pandas DataFrame.

    The spelling of the function name is historical.

    Parameters
    ----------
    df : pandas.DataFrame
        Table to transpose.
    index_col : str or list of str
        Column(s) set as the index before the transposition; their values become the column labels of the result.

    Returns
    -------
    pandas.DataFrame
        ``df.set_index(index_col).T.reset_index()``: the former column names are in a leading column named ``index``.
    """
    df = df.set_index(index_col).T.reset_index()
    return df

convert_perc_str_to_float

convert_perc_str_to_float(df, cols)

Percentage to Float.

Parameters:

Name Type Description Default
df DataFrame

Table to convert.

required
cols list of str

Columns to convert. A column is converted only when its dtype is object (percentage text such as "12.5%"): the trailing % is removed and the number is divided by 100. Columns of any other dtype are left unchanged.

required

Returns:

Type Description
DataFrame

df itself; the columns are replaced in place.

Source code in ExcelMaster/Utility.py
def convert_perc_str_to_float(df, cols):
    """Percentage to Float.

    Parameters
    ----------
    df : pandas.DataFrame
        Table to convert.
    cols : list of str
        Columns to convert. A column is converted only when its dtype is ``object`` (percentage text such as
        ``"12.5%"``): the trailing ``%`` is removed and the number is divided by 100. Columns of any other dtype are left
        unchanged.

    Returns
    -------
    pandas.DataFrame
        ``df`` itself; the columns are replaced in place.
    """
    for col in cols:
        if str(df[col].dtypes) == 'object':
            df[col] = df[col].str.rstrip('%').astype('float') / 100
    return df

color_hex2rgb

color_hex2rgb(hex_code)

Convert Color Hex Code to RGB Tuple.

Parameters:

Name Type Description Default
hex_code str

Color as six hexadecimal digits with an optional leading # ("#FF8800" or "ff8800"). The three-digit shorthand is not supported.

required

Returns:

Type Description
tuple of int

(red, green, blue) with values from 0 to 255, for example (255, 136, 0).

Source code in ExcelMaster/Utility.py
def color_hex2rgb(hex_code):
    """Convert Color Hex Code to RGB Tuple.

    Parameters
    ----------
    hex_code : str
        Color as six hexadecimal digits with an optional leading ``#`` (``"#FF8800"`` or ``"ff8800"``). The three-digit
        shorthand is not supported.

    Returns
    -------
    tuple of int
        ``(red, green, blue)`` with values from 0 to 255, for example ``(255, 136, 0)``.
    """
    hex_code = hex_code.lower()
    h = hex_code.lstrip('#')
    return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))

get_color_set

get_color_set(n, start_num=0, step=1, retName=False, lookupName=None)

Return a set of color code without replacement.

Picks colors from the XKCD color table of matplotlib (949 named colors).

Parameters:

Name Type Description Default
n int

Number of colors.

required
start_num int

Index in the XKCD table of the first color.

0
step int

Distance between two consecutive picked indices (start_num, start_num + step, ...). An index beyond the end of the table raises IndexError.

1
retName bool

If True, return a dict {color name: hex code} instead of a list of hex codes.

False
lookupName str or None

Regular expression that replaces the index-based picks: the first n colors whose table key matches it (re.search) are returned, fewer if fewer match. The key carries the "xkcd:" prefix ("xkcd:blue"), so the prefix is part of the searched text. start_num and step are then not used, but the index-based loop still runs first and can raise IndexError.

None

Returns:

Type Description
list of str or dict

Hex color codes ("#acc2d9"), or {name: code} when retName is True.

Source code in ExcelMaster/Utility.py
def get_color_set(n, start_num = 0, step = 1, retName = False, lookupName = None):
    """Return a set of color code without replacement.

    Picks colors from the XKCD color table of matplotlib (949 named colors).

    Parameters
    ----------
    n : int
        Number of colors.
    start_num : int, default 0
        Index in the XKCD table of the first color.
    step : int, default 1
        Distance between two consecutive picked indices (``start_num``, ``start_num + step``, ...). An index beyond the
        end of the table raises ``IndexError``.
    retName : bool, default False
        If True, return a dict ``{color name: hex code}`` instead of a list of hex codes.
    lookupName : str or None, default None
        Regular expression that replaces the index-based picks: the first ``n`` colors whose table key matches it
        (``re.search``) are returned, fewer if fewer match. The key carries the ``"xkcd:"`` prefix (``"xkcd:blue"``), so
        the prefix is part of the searched text. ``start_num`` and ``step`` are then not used, but the index-based loop still
        runs first and can raise ``IndexError``.

    Returns
    -------
    list of str or dict
        Hex color codes (``"#acc2d9"``), or ``{name: code}`` when ``retName`` is True.
    """
    import matplotlib.colors as mcolors
    import re

    colors = list(mcolors.XKCD_COLORS.items())
    color_set = {}

    for i in range(start_num, start_num + n * step, step):
        name = colors[i][0].replace("xkcd:", "")
        color = colors[i][1]
        color_set[name] = color

    if lookupName:
        color_set = {}
        i = 0
        for name, color in colors:
            if len(color_set.items()) == n:
                break
            if re.search(lookupName, name):
                color_set[name.replace("xkcd:", "")] = color
            i += 1

    if retName:
        return color_set
    return list(color_set.values())

string_proc

string_proc(x)

Process a given String.

Parameters:

Name Type Description Default
x str

Text whose words are separated by underscores, for example a column name.

required

Returns:

Type Description
str

The words capitalized (first letter upper-case, the rest lower-case) and joined by single spaces, for example "credit_SCORE_v2" becomes "Credit Score V2".

Source code in ExcelMaster/Utility.py
def string_proc(x):
    """Process a given String.

    Parameters
    ----------
    x : str
        Text whose words are separated by underscores, for example a column name.

    Returns
    -------
    str
        The words capitalized (first letter upper-case, the rest lower-case) and joined by single spaces, for example
        ``"credit_SCORE_v2"`` becomes ``"Credit Score V2"``.
    """
    x_list = x.split("_")
    x_list = [x.strip().capitalize() for x in x_list]
    x = " ".join(x_list)
    return x

convert_to_boxplot_data

convert_to_boxplot_data(df, x, y, y_percentage=False)

Convert dataframe to boxplot data.

Parameters:

Name Type Description Default
df DataFrame

Source table.

required
x str

Column that defines the groups (one box per distinct value).

required
y str

Column with the values of each group.

required
y_percentage bool

If True, multiply every value by 100.

False

Returns:

Type Description
dict

{group value: list of y values}, with the groups in ascending order of x.

Source code in ExcelMaster/Utility.py
def convert_to_boxplot_data(df, x, y, y_percentage = False):
    """Convert dataframe to boxplot data.

    Parameters
    ----------
    df : pandas.DataFrame
        Source table.
    x : str
        Column that defines the groups (one box per distinct value).
    y : str
        Column with the values of each group.
    y_percentage : bool, default False
        If True, multiply every value by 100.

    Returns
    -------
    dict
        ``{group value: list of y values}``, with the groups in ascending order of ``x``.
    """
    x_unique_value = df[x].sort_values().unique().tolist()

    box_plot_data = {}
    for v in x_unique_value:
        box_plot_data[v] = [num * 100 if y_percentage else num for num in df[df[x] == v][y].tolist()]
    return box_plot_data

color_input_validation

color_input_validation(color_grp, val_n)

Perform Color Input Validation.

Parameters:

Name Type Description Default
color_grp tuple or str or list of str

A 2-tuple (start_num, step) picks val_n named colors from the XKCD table with get_color_set; a string is one color code used for all boxes; a list of strings is used as it is and must have exactly val_n entries.

required
val_n int

Number of colors needed (number of boxes).

required

Returns:

Type Description
list of str

Color codes, one per box.

Raises:

Type Description
ValueError

If color_grp has none of the three accepted forms (this includes a list whose length is not val_n).

Source code in ExcelMaster/Utility.py
def color_input_validation(color_grp, val_n):
    """Perform Color Input Validation.

    Parameters
    ----------
    color_grp : tuple or str or list of str
        A 2-tuple ``(start_num, step)`` picks ``val_n`` named colors from the XKCD table with ``get_color_set``; a string is
        one color code used for all boxes; a list of strings is used as it is and must have exactly ``val_n`` entries.
    val_n : int
        Number of colors needed (number of boxes).

    Returns
    -------
    list of str
        Color codes, one per box.

    Raises
    ------
    ValueError
        If ``color_grp`` has none of the three accepted forms (this includes a list whose length is not ``val_n``).
    """
    if isinstance(color_grp, tuple) and len(color_grp) == 2:
        # Customize colors
        cols = get_color_set(val_n, color_grp[0], color_grp[1], False)
        colors = cols
    elif isinstance(color_grp, str):
        colors = [color_grp] * val_n
    elif (isinstance(color_grp, list)) and (all([isinstance(x, str) for x in color_grp])) and (len(color_grp) == val_n):
        colors = color_grp
    else:
        raise ValueError("Please give valid color_grp: tuple of two numbers, list of color code or a single color code.")
    return colors

get_metric_shift

get_metric_shift(data, metric_name, nvars_col='nvars')

Calculate Metric Shift for Variable Reduction.

For every row, the slope between consecutive rows: (metric - metric of the previous row) / (nvars - nvars of the previous row), that is the change of the metric per variable added (or removed, when the rows are ordered by decreasing number of variables).

Parameters:

Name Type Description Default
data DataFrame

Table with one row per model, ordered by the number of variables.

required
metric_name str

Column holding the metric.

required
nvars_col str

Column holding the number of variables.

"nvars"

Returns:

Type Description
Series

The shift of every row, with the index of data. The first row is 0 (missing values are filled with 0). Rows whose number of variables equals that of the previous row divide by zero: inf / -inf, or 0 when the metric is unchanged too.

Source code in ExcelMaster/Utility.py
def get_metric_shift(data, metric_name, nvars_col = "nvars"):
    """Calculate Metric Shift for Variable Reduction.

    For every row, the slope between consecutive rows: ``(metric - metric of the previous row) / (nvars - nvars of the
    previous row)``, that is the change of the metric per variable added (or removed, when the rows are ordered by
    decreasing number of variables).

    Parameters
    ----------
    data : pandas.DataFrame
        Table with one row per model, ordered by the number of variables.
    metric_name : str
        Column holding the metric.
    nvars_col : str, default "nvars"
        Column holding the number of variables.

    Returns
    -------
    pandas.Series
        The shift of every row, with the index of ``data``. The first row is 0 (missing values are filled with 0). Rows
        whose number of variables equals that of the previous row divide by zero: ``inf`` / ``-inf``, or 0 when the metric
        is unchanged too.
    """
    metric_lift = (data[metric_name] - data[metric_name].shift(1))
    nvars_reduced = (data[nvars_col] - data[nvars_col].shift(1))
    return (metric_lift / nvars_reduced).fillna(0)

get_metrics_shift

get_metrics_shift(data, metric_cols)

Get Shift for List of Metrics.

Adds a column <metric>_shift for every metric, computed with get_metric_shift and its default nvars_col="nvars", so data needs a column named nvars.

Parameters:

Name Type Description Default
data DataFrame

Table with one row per model, ordered by the number of variables.

required
metric_cols list of str

Metric columns.

required

Returns:

Type Description
DataFrame

data itself with the added *_shift columns (modified in place).

Source code in ExcelMaster/Utility.py
def get_metrics_shift(data, metric_cols):
    """Get Shift for List of Metrics.

    Adds a column ``<metric>_shift`` for every metric, computed with ``get_metric_shift`` and its default
    ``nvars_col="nvars"``, so ``data`` needs a column named ``nvars``.

    Parameters
    ----------
    data : pandas.DataFrame
        Table with one row per model, ordered by the number of variables.
    metric_cols : list of str
        Metric columns.

    Returns
    -------
    pandas.DataFrame
        ``data`` itself with the added ``*_shift`` columns (modified in place).
    """
    for metric in metric_cols:
        if metric.startswith(tuple(metric_cols)):
            data[metric+"_shift"] = get_metric_shift(data, metric)
    return data

compute_overfitting_shift

compute_overfitting_shift(data, sample_prefix)

Calculate Overfitting Performance Shift.

Parameters:

Name Type Description Default
data DataFrame

Table with the metric columns of both samples, named <prefix><metric>.

required
sample_prefix tuple of str

(base_prefix, other_prefix). The columns whose names start with sample_prefix[0] and those that start with sample_prefix[1] are paired in column order (not by name).

required

Returns:

Type Description
DataFrame

data itself (modified in place) with one added column <other column>_shift per pair, equal to other / base - 1: the relative change of the second sample versus the first.

Raises:

Type Description
ValueError

If the two prefixes select a different number of columns.

Source code in ExcelMaster/Utility.py
def compute_overfitting_shift(data, sample_prefix):
    """Calculate Overfitting Performance Shift.

    Parameters
    ----------
    data : pandas.DataFrame
        Table with the metric columns of both samples, named ``<prefix><metric>``.
    sample_prefix : tuple of str
        ``(base_prefix, other_prefix)``. The columns whose names start with ``sample_prefix[0]`` and those that start with
        ``sample_prefix[1]`` are paired in column order (not by name).

    Returns
    -------
    pandas.DataFrame
        ``data`` itself (modified in place) with one added column ``<other column>_shift`` per pair, equal to
        ``other / base - 1``: the relative change of the second sample versus the first.

    Raises
    ------
    ValueError
        If the two prefixes select a different number of columns.
    """
    b_metrics = [x for x in data.columns if x.startswith(sample_prefix[0])]
    o_metrics = [x for x in data.columns if x.startswith(sample_prefix[1])]

    if len(b_metrics) == len(o_metrics):
        for b_metric, o_metric in zip(b_metrics, o_metrics):
            data[o_metric+"_shift"] = data[o_metric].div(data[b_metric]) - 1
        return data

    raise ValueError("The lengths of metrics between two samples are not the same in the given dataset!")

proc_psi_raw_report

proc_psi_raw_report(psi_raw_table, psi_title, keep_list=None, varname='variable', upper=True)

Processing Raw PSI Report generated from Takecopter.

Parameters:

Name Type Description Default
psi_raw_table str or DataFrame

Raw PSI table: a DataFrame or the path of a csv / sas7bdat file, read with input_validation (so the column names are lower-cased). It must contain the column var_for_psi.

required
psi_title str

Title placed on the top level of the two-level column header of the result.

required
keep_list list of str or None

Columns to keep, spelled as in the processed table (upper-case when upper is True). None or an empty list keeps all columns.

None
varname str

New name of the var_for_psi column, which becomes the index.

"variable"
upper bool

If True, upper-case the names of the remaining columns.

True

Returns:

Type Description
DataFrame

Table indexed by varname whose columns are the two-level pairs (psi_title, column name).

Source code in ExcelMaster/Utility.py
def proc_psi_raw_report(psi_raw_table, psi_title, keep_list = None, varname = "variable", upper=True):
    """Processing Raw PSI Report generated from Takecopter.

    Parameters
    ----------
    psi_raw_table : str or pandas.DataFrame
        Raw PSI table: a DataFrame or the path of a csv / sas7bdat file, read with ``input_validation`` (so the column
        names are lower-cased). It must contain the column ``var_for_psi``.
    psi_title : str
        Title placed on the top level of the two-level column header of the result.
    keep_list : list of str or None, default None
        Columns to keep, spelled as in the processed table (upper-case when ``upper`` is True). None or an empty list keeps
        all columns.
    varname : str, default "variable"
        New name of the ``var_for_psi`` column, which becomes the index.
    upper : bool, default True
        If True, upper-case the names of the remaining columns.

    Returns
    -------
    pandas.DataFrame
        Table indexed by ``varname`` whose columns are the two-level pairs ``(psi_title, column name)``.
    """
    psi_table = input_validation(psi_raw_table)
    psi_table = psi_table.rename(columns={"var_for_psi":varname})
    psi_table = psi_table.set_index(varname)
    if upper:
        psi_table.columns = [x.upper() for x in psi_table.columns]
    psi_table.columns = [[psi_title]*len(psi_table.columns),psi_table.columns]
    if keep_list:
        psi_table = psi_table[[(psi_title, x) for x in keep_list]]
    return psi_table

get_mean_risk

get_mean_risk(bivar_single_attr, value_range_col=['min_indep', 'max_indep'], dep_col='dep')

get average risk

Parameters:

Name Type Description Default
bivar_single_attr DataFrame

Bivariate table of one attribute with one row per bin.

required
value_range_col list of str

Columns with the bounds of each bin. A row whose bounds are all missing is the missing-value bin and is left out of mean_no_nan.

['min_indep', 'max_indep']
dep_col str

Column with the bad rate of each bin.

"dep"

Returns:

Type Description
DataFrame

bivar_single_attr itself with two constant columns added in place: mean (mean of dep_col over all rows) and mean_no_nan (mean over the rows that have at least one bin bound). Both are plain averages of the per-bin values, not weighted by bin size.

Source code in ExcelMaster/Utility.py
def get_mean_risk(bivar_single_attr, value_range_col = ['min_indep', 'max_indep'], dep_col = "dep"):
    """get average risk

    Parameters
    ----------
    bivar_single_attr : pandas.DataFrame
        Bivariate table of one attribute with one row per bin.
    value_range_col : list of str, default ['min_indep', 'max_indep']
        Columns with the bounds of each bin. A row whose bounds are all missing is the missing-value bin and is left out of
        ``mean_no_nan``.
    dep_col : str, default "dep"
        Column with the bad rate of each bin.

    Returns
    -------
    pandas.DataFrame
        ``bivar_single_attr`` itself with two constant columns added in place: ``mean`` (mean of ``dep_col`` over all rows)
        and ``mean_no_nan`` (mean over the rows that have at least one bin bound). Both are plain averages of the per-bin
        values, not weighted by bin size.
    """
    mean_wo_nan = bivar_single_attr.dropna(how = "all", subset=value_range_col)[dep_col].mean()
    mean_w_na = bivar_single_attr[dep_col].mean()
    bivar_single_attr["mean"] = mean_w_na
    bivar_single_attr["mean_no_nan"] = mean_wo_nan
    return bivar_single_attr

get_pva_report

get_pva_report(em, ws, gains_result, sample_list, nbins=10, varcol='variable', chart_scale=(10, 10))

Plot PVA Table by Segment across Sample.

Writes a "PVA Charts" heading and then, for every segment, a heading with the segment name, one line chart per sample side by side (predicted mean_score against actual interval_bad_rate by rank) and, to the right of the charts, the pivoted table (rank by sample and predicted / actual).

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
gains_result str or DataFrame

Gains table: a DataFrame or the path of a csv / sas7bdat file, read with input_validation (so the column names are lower-cased). It needs the columns seg_name, sample, rank, mean_score and interval_bad_rate (percentage text such as "12.5%" or numbers). Within a segment, rows that have a missing value in any column are dropped.

required
sample_list list of str

Values of the sample column to chart: one chart per sample and segment. The upper-cased value appears in the chart title.

required
nbins int

Number of bins (ranks) of the gains table. It only sizes the charts: chart_size in cells is (nbins + chart_scale[0], (nbins + chart_scale[1]) / 2).

10
varcol str

Name of the column that holds the melted series names. It must stay "variable", the name pandas gives to that column; any other value raises KeyError.

"variable"
chart_scale tuple of int

Extra (rows, columns) added to nbins when sizing the charts.

(10, 10)

Returns:

Type Description
Worksheet

ws.

Notes

Sets em.gap_number to 2. The charts use a fixed y axis from 0 to 1 (formatted as 0.00%) and the table values are formatted as percentages with 3 decimals. After each segment the cursor is moved below the charts, in the column of the first chart.

Source code in ExcelMaster/Template.py
def get_pva_report(em, ws, gains_result, sample_list, nbins = 10, varcol = "variable", chart_scale = (10, 10)):
    """Plot PVA Table by Segment across Sample.

    Writes a "PVA Charts" heading and then, for every segment, a heading with the segment name, one line chart per sample
    side by side (predicted ``mean_score`` against actual ``interval_bad_rate`` by rank) and, to the right of the charts,
    the pivoted table (rank by sample and predicted / actual).

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    gains_result : str or pandas.DataFrame
        Gains table: a DataFrame or the path of a csv / sas7bdat file, read with ``input_validation`` (so the column names
        are lower-cased). It needs the columns ``seg_name``, ``sample``, ``rank``, ``mean_score`` and
        ``interval_bad_rate`` (percentage text such as ``"12.5%"`` or numbers). Within a segment, rows that have a missing
        value in any column are dropped.
    sample_list : list of str
        Values of the ``sample`` column to chart: one chart per sample and segment. The upper-cased value appears in the
        chart title.
    nbins : int, default 10
        Number of bins (ranks) of the gains table. It only sizes the charts: ``chart_size`` in cells is
        ``(nbins + chart_scale[0], (nbins + chart_scale[1]) / 2)``.
    varcol : str, default "variable"
        Name of the column that holds the melted series names. It must stay ``"variable"``, the name pandas gives to that
        column; any other value raises ``KeyError``.
    chart_scale : tuple of int, default (10, 10)
        Extra ``(rows, columns)`` added to ``nbins`` when sizing the charts.

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    Sets ``em.gap_number`` to 2. The charts use a fixed y axis from 0 to 1 (formatted as ``0.00%``) and the table values
    are formatted as percentages with 3 decimals. After each segment the cursor is moved below the charts, in the column
    of the first chart.
    """
    gains_results = input_validation(gains_result)

    varcol = varcol.lower()
    segs = gains_results["seg_name"].unique().tolist()
    chart_size = (nbins + chart_scale[0], (nbins + chart_scale[1])/2)

    # em.reset_curr_loc()
    em.merge_col(worksheet=ws, ncols=5, text="PVA Charts")

    em.gap_number = 2
    for seg in segs:
        """ Plot PVA charts by segment row by row """
        ########## Data Wrangling ###############
        example = gains_results.query(f"seg_name == '{seg}'").dropna()
        example = convert_perc_str_to_float(example, ["interval_bad_rate"])

        cols = ["rank", "sample", "mean_score", "interval_bad_rate"]

        example_fnl = example[cols].astype({"rank":int}).melt(id_vars = ["rank", "sample"], value_vars = ["mean_score", "interval_bad_rate"])
        example_fnl["score_type"] = example_fnl[varcol].str.replace("mean_score", "predicted").replace("interval_bad_rate", "actual")
        example_fnl = example_fnl.pivot(index = ["rank"], columns=["sample", "score_type"], values = "value")
        col_order = example_fnl.columns.get_level_values(0).unique().tolist()
        fnl_out_res = example_fnl[col_order]

        ########### Insert Data and Charts to the Worksheet ###################
        em.write_text_content(worksheet=ws, input_text=f"{seg} [[ORANGE_H3]] \n")

        # sample_list = example["sample"].unique().tolist()
        chart_loc = {}
        for sample in sample_list:
            """ Plot PVA charts by sample column by column"""
            example_sample = example.query(f"sample == '{sample}'").dropna()
            loc = em.write_chart(ws, df = example_sample, x="rank", 
                                  y_list=["mean_score", "interval_bad_rate"], 
                                  title=f"PVA Chart ({sample.upper()}) {seg}", 
                                  chart_size=chart_size, 
                                  chart_type="line", 
                                  major_gridlines=False, 
                                  xy_axes_name=("Quantiles", "% of Bads"), 
                                  y_axis_range=(0, 1), 
                                  y_num_format="0.00%",
                                  skipby="col", retCellRange="value")
            chart_loc[sample] = loc

        loc4 = em.write_dataframe(worksheet=ws, df=fnl_out_res, title=f"PVA Chart ({seg})", index=True, skipby="row", retCellRange="value")

        tbl_value_range = [x + 2 if i == 0 else x + 1 
                                 if i == 1 else x + 2
                                 if i == 2 else x
                           for i, x in enumerate(loc4)]

        em.set_cell_format(ws, tbl_value_range, "----")
        em.set_cell_format(ws, tbl_value_range, "NUM%.3")

        chart1_loc = chart_loc[sample_list[0]]
        em.curr_row = chart1_loc[2] + em.gap_number
        em.curr_col = chart1_loc[1]
    return ws

get_bivar_report

get_bivar_report(em, ws, attr_info, bivar, sample_list, varcol='varname', sample_col='sample', x_cols=['min_indep', 'max_indep'], n_col='_freq_', dep_col='dep', chart_size=(20, 10), average_risk_line=False)

Get Bivar Report by Attribute across Samples.

Writes a "Bivar Plot" heading and then, for every attribute of attr_info (in the row order of that table), the attribute's information table and, side by side for every sample, a combined chart: the number of observations per bin as columns (primary axis) and the bad rate as a line (secondary axis, 0.00%), with the bin ranges [min, max] on the x axis.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
attr_info str or DataFrame

Attribute information: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by input_validation). One row per attribute, with the attribute names in UPPER case in column varcol and a description column (used as the x axis title). All columns are written in the attribute's information table.

required
bivar str or DataFrame

Bivariate table with one row per bin and sample (path or DataFrame, column names lower-cased), with the columns varcol (attribute names in lower case), sample_col, the two x_cols, n_col and dep_col.

required
sample_list list of str

Values of sample_col to chart: one chart per attribute and sample.

required
varcol str

Name of the attribute column in attr_info and bivar.

"varname"
sample_col str

Name of the sample column in bivar.

"sample"
x_cols list of str

The two columns with the lower and the upper bound of each bin; they build the x axis labels "[min, max]".

["min_indep", "max_indep"]
n_col str

Column with the number of observations of each bin (drawn as columns).

"_freq_"
dep_col str

Column with the bad rate of each bin (drawn as a line).

"dep"
chart_size tuple of int

(rows, columns) of each chart, in cells.

(20, 10)
average_risk_line bool

If True, two dashed lines are added to each chart: the mean bad rate over all bins (mean) and over the bins that have a bin range, that is without the missing-value bin (mean_no_nan).

False

Returns:

Type Description
Worksheet

ws.

Notes
  • attr_info is filtered with the upper-cased attribute name and bivar with the lower-cased one, so the attribute names must be upper case in attr_info and lower case in bivar. Lower-case names in attr_info raise IndexError; upper-case names in bivar silently give empty charts.
  • The mean lines come from get_mean_risk with its default column names, whatever x_cols and dep_col are: bivar must always contain the columns min_indep, max_indep and dep (KeyError otherwise), even when average_risk_line is False.
  • Sets em.gap_number to 2. After each attribute the cursor is moved below the charts, in the column of the first chart.
Source code in ExcelMaster/Template.py
def get_bivar_report(em, ws, attr_info, bivar, sample_list, varcol = "varname", sample_col = "sample", x_cols = ["min_indep", "max_indep"], n_col = "_freq_", dep_col = "dep", chart_size = (20, 10), average_risk_line = False):
    """Get Bivar Report by Attribute across Samples.

    Writes a "Bivar Plot" heading and then, for every attribute of ``attr_info`` (in the row order of that table), the
    attribute's information table and, side by side for every sample, a combined chart: the number of observations per bin
    as columns (primary axis) and the bad rate as a line (secondary axis, ``0.00%``), with the bin ranges ``[min, max]``
    on the x axis.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    attr_info : str or pandas.DataFrame
        Attribute information: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by
        ``input_validation``). One row per attribute, with the attribute names in UPPER case in column ``varcol`` and a
        ``description`` column (used as the x axis title). All columns are written in the attribute's information table.
    bivar : str or pandas.DataFrame
        Bivariate table with one row per bin and sample (path or DataFrame, column names lower-cased), with the columns
        ``varcol`` (attribute names in lower case), ``sample_col``, the two ``x_cols``, ``n_col`` and ``dep_col``.
    sample_list : list of str
        Values of ``sample_col`` to chart: one chart per attribute and sample.
    varcol : str, default "varname"
        Name of the attribute column in ``attr_info`` and ``bivar``.
    sample_col : str, default "sample"
        Name of the sample column in ``bivar``.
    x_cols : list of str, default ["min_indep", "max_indep"]
        The two columns with the lower and the upper bound of each bin; they build the x axis labels ``"[min, max]"``.
    n_col : str, default "_freq_"
        Column with the number of observations of each bin (drawn as columns).
    dep_col : str, default "dep"
        Column with the bad rate of each bin (drawn as a line).
    chart_size : tuple of int, default (20, 10)
        ``(rows, columns)`` of each chart, in cells.
    average_risk_line : bool, default False
        If True, two dashed lines are added to each chart: the mean bad rate over all bins (``mean``) and over the bins
        that have a bin range, that is without the missing-value bin (``mean_no_nan``).

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    * ``attr_info`` is filtered with the upper-cased attribute name and ``bivar`` with the lower-cased one, so the
      attribute names must be upper case in ``attr_info`` and lower case in ``bivar``. Lower-case names in ``attr_info``
      raise ``IndexError``; upper-case names in ``bivar`` silently give empty charts.
    * The mean lines come from ``get_mean_risk`` with its default column names, whatever ``x_cols`` and ``dep_col`` are:
      ``bivar`` must always contain the columns ``min_indep``, ``max_indep`` and ``dep`` (``KeyError`` otherwise), even
      when ``average_risk_line`` is False.
    * Sets ``em.gap_number`` to 2. After each attribute the cursor is moved below the charts, in the column of the first
      chart.
    """

    ##### Worksheet 1 ######
    em.merge_col(worksheet = ws, ncols=5, text = "Bivar Plot")

    bivar_table = input_validation(bivar)
    attr_info_table = input_validation(attr_info)

    varcol = varcol.lower()
    sample_col = sample_col.lower()
    x_cols = [x.lower() for x in x_cols]
    n_col = n_col.lower()
    dep_col = dep_col.lower()

    bivar_table.columns = [x.lower() for x in bivar_table.columns]
    attr_info_table.columns = [x.lower() for x in attr_info_table.columns]

    var_by_varimp = attr_info_table[varcol].str.lower().tolist()
    bivar_table["indep_range"] = "[" + bivar_table[x_cols[0]].astype(str) + ", " \
                                     + bivar_table[x_cols[1]].astype(str) + "]"

    for var in var_by_varimp:
        """ Plot Bivar Chart by Variable Row by Row. """

        em.gap_number = 1
        singe_attr_info = attr_info_table.query(f"{varcol} == '{var.upper()}'")
        em.write_dataframe(ws, df = singe_attr_info, index = False, 
                           title =f"Information for Attribute {var.upper()}", skipby="row", retCellRange="value")
        var_desc = singe_attr_info["description"].values[0]

        em.gap_number = 2
        chart_loc = {}
        for sample in sample_list:
            """ Plot Bivar Chart Column by Column """
            single_attr_bivar_sample = bivar_table.query(f"{varcol} == '{var}' and {sample_col} == '{sample}'")
            single_attr_bivar_sample = get_mean_risk(single_attr_bivar_sample)

            loc = em.write_duo_chart(worksheet = ws, 
                                       df = single_attr_bivar_sample, 
                                       y1_list = [n_col], 
                                       y2_list = [dep_col], 
                                       x = "indep_range",
                                       c1_type = "column", 
                                       c2_type = "line", 
                                       y1_axis_range = None,
                                       y2_axis_range = None, 
                                       title = f"Bivar Plot ({sample.upper()}) for Attribute {var.upper()}", 
                                       chart_size = chart_size,
                                       xy_axes_name = (var_desc, "N", "Bad Rate"),
                                       major_gridlines=False,
                                       retChart = average_risk_line, 
                                       y2_num_format = "0.00%",
                                       skipby = "col", 
                                       retCellRange="value")

            if average_risk_line:

                column_chart = loc[0]
                line_chart = loc[1]
                fnl_line_chart = em.write_chart(worksheet = ws, 
                                           df = single_attr_bivar_sample, 
                                           y_list = ['mean', 'mean_no_nan'], 
                                           x = "indep_range",
                                           chart_type = "line", 
                                           chart_size = chart_size,
                                           y_num_format = "0.00%", 
                                           line_type = "long_dash",
                                           line_marker = "triangle",
                                           xy_axes_name = (var_desc, "N", "Bad Rate"),
                                           major_gridlines=False,
                                           retChart = True,
                                           y2_axis = True,
                                           append_to_chart = line_chart)

                loc = em.write_combined_chart(ws, 
                                              chart1 = column_chart, 
                                              chart2 = fnl_line_chart, 
                                              chart_size = chart_size, 
                                              skipby = "col", retCellRange="value")
            chart_loc[sample] = loc
        loc1 = chart_loc[sample_list[0]]
        em.curr_row = loc1[2] + em.gap_number
        em.curr_col = loc1[1]
    return ws

add_scr_info

add_scr_info(em, ws, data, info, scr_info, sample_prefices, wTitle=True)

Add Score Info Comparison

Writes, from left to right starting at the cursor, a block of identifying information followed by one block of score statistics per sample.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The blocks start at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
data DataFrame

One row per score or segment, with the columns named in info and, for every prefix of sample_prefices, the columns <prefix><name> for each name of scr_info.

required
info list of str

Identifying columns, at least three: the first three become the (multi-level) index of the first block, the others are its columns.

required
scr_info list of str

Statistic names without the sample prefix (for example ["num_of_total", "num_of_bad", "rate_of_bad", "mean_score"]). The prefix is removed from the column headers that are written.

required
sample_prefices list of str

Column prefixes of the samples (for example ["train_", "test_"]). Each prefix gives one block, titled with the prefix in upper case without surrounding underscores.

required
wTitle bool

If True, write a title row over every block (a blank title over the information block). If False, no title row is written, which suits blocks that are stacked under blocks that already have titles.

True

Returns:

Type Description
tuple of int

(first_col, next_row): the column of the first block and the row directly below the blocks (the column comes first). The cursor of em is set to the same position.

Notes

Sets em.gap_number to 0, so the blocks touch each other. The NUM%.2 format is applied from the third column of each sample block on; the first two columns are left unformatted.

Source code in ExcelMaster/Template.py
def add_scr_info(em, ws, data, info, scr_info, sample_prefices, wTitle = True):
    """Add Score Info Comparison

    Writes, from left to right starting at the cursor, a block of identifying information followed by one block of score
    statistics per sample.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The blocks start at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    data : pandas.DataFrame
        One row per score or segment, with the columns named in ``info`` and, for every prefix of ``sample_prefices``, the
        columns ``<prefix><name>`` for each name of ``scr_info``.
    info : list of str
        Identifying columns, at least three: the first three become the (multi-level) index of the first block, the others
        are its columns.
    scr_info : list of str
        Statistic names without the sample prefix (for example ``["num_of_total", "num_of_bad", "rate_of_bad",
        "mean_score"]``). The prefix is removed from the column headers that are written.
    sample_prefices : list of str
        Column prefixes of the samples (for example ``["train_", "test_"]``). Each prefix gives one block, titled with the
        prefix in upper case without surrounding underscores.
    wTitle : bool, default True
        If True, write a title row over every block (a blank title over the information block). If False, no title row is
        written, which suits blocks that are stacked under blocks that already have titles.

    Returns
    -------
    tuple of int
        ``(first_col, next_row)``: the column of the first block and the row directly below the blocks (the column comes
        first). The cursor of ``em`` is set to the same position.

    Notes
    -----
    Sets ``em.gap_number`` to 0, so the blocks touch each other. The ``NUM%.2`` format is applied from the third column of
    each sample block on; the first two columns are left unformatted.
    """
    em.gap_number = 0
    title=" "
    if not wTitle:
        title = None
    info_data = data[info].set_index(info[:3])
    info_loc = em.write_dataframe(ws, df=info_data, retCellRange="value", skipby="col", title=title, titleformat="", index = True)

    i = 0
    while i < len(sample_prefices):
        title=sample_prefices[i].upper().strip("_")
        if not wTitle:
            title = None
        tmp = data[[sample_prefices[i] + x for x in scr_info]]
        tmp.columns = [x.replace(sample_prefices[i], "") for x in tmp.columns]
        loc = em.write_dataframe(ws, df=tmp, retCellRange="value", skipby="col", title=title)
        em.set_cell_format(ws, cell_range=[x + 2 if i == 1 else x for i, x in enumerate(loc)], cformat="NUM%.2")
        i += 1

    em.curr_col = info_loc[1]
    em.curr_row = loc[2] + 1
    return (info_loc[1], loc[2] + 1)

add_perf_metrics

add_perf_metrics(em, ws, data, info, perf_metrics, sample_prefices, wTitle=True)

Add Perf Metrics Comparison

Writes, from left to right starting at the cursor, a block of identifying information followed by one block of performance metrics per sample.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The blocks start at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
data DataFrame

One row per score or segment, with the columns named in info and, for every prefix of sample_prefices, the columns <prefix><metric> for each metric of perf_metrics.

required
info list of str

Identifying columns, at least three: the first three become the (multi-level) index of the first block, the others are its columns.

required
perf_metrics list of str

Metric names without the sample prefix (for example ["ks", "top10_cap", "auc"]). The prefix is removed from the column headers that are written.

required
sample_prefices list of str

Column prefixes of the samples (for example ["train_", "test_"]). Each prefix gives one block, titled with the prefix in upper case without surrounding underscores.

required
wTitle bool

If True, write a title row over every block (a blank title over the information block). If False, no title row is written, which suits blocks that are stacked under blocks that already have titles.

True

Returns:

Type Description
tuple of int

(first_col, next_row): the column of the first block and the row directly below the blocks (the column comes first). The cursor of em is set to the same position.

Notes

Sets em.gap_number to 0, so the blocks touch each other. The NUM%.2 format is applied to every metric column of a sample block except the last one.

Source code in ExcelMaster/Template.py
def add_perf_metrics(em, ws, data, info, perf_metrics, sample_prefices, wTitle = True):
    """Add Perf Metrics Comparison

    Writes, from left to right starting at the cursor, a block of identifying information followed by one block of
    performance metrics per sample.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The blocks start at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    data : pandas.DataFrame
        One row per score or segment, with the columns named in ``info`` and, for every prefix of ``sample_prefices``, the
        columns ``<prefix><metric>`` for each metric of ``perf_metrics``.
    info : list of str
        Identifying columns, at least three: the first three become the (multi-level) index of the first block, the others
        are its columns.
    perf_metrics : list of str
        Metric names without the sample prefix (for example ``["ks", "top10_cap", "auc"]``). The prefix is removed from the
        column headers that are written.
    sample_prefices : list of str
        Column prefixes of the samples (for example ``["train_", "test_"]``). Each prefix gives one block, titled with the
        prefix in upper case without surrounding underscores.
    wTitle : bool, default True
        If True, write a title row over every block (a blank title over the information block). If False, no title row is
        written, which suits blocks that are stacked under blocks that already have titles.

    Returns
    -------
    tuple of int
        ``(first_col, next_row)``: the column of the first block and the row directly below the blocks (the column comes
        first). The cursor of ``em`` is set to the same position.

    Notes
    -----
    Sets ``em.gap_number`` to 0, so the blocks touch each other. The ``NUM%.2`` format is applied to every metric column of
    a sample block except the last one.
    """
    em.gap_number = 0
    title=" "
    if not wTitle:
        title = None
    info_data = data[info].set_index(info[:3])
    info_loc = em.write_dataframe(ws, df=info_data, retCellRange="value", skipby="col", title=title, titleformat="", index = True)

    i = 0
    while i < len(sample_prefices):
        title=sample_prefices[i].upper().strip("_")
        if not wTitle:
            title = None
        tmp = data[[sample_prefices[i] + x for x in perf_metrics]]
        tmp.columns = [x.replace(sample_prefices[i], "") for x in tmp.columns]
        loc = em.write_dataframe(ws, df=tmp, retCellRange="value", skipby="col", title=title)
        em.set_cell_format(ws, cell_range=[x - 1 if i == 3 else x for i, x in enumerate(loc)], cformat="NUM%.2")
        i += 1

    em.curr_col = info_loc[1]
    em.curr_row = loc[2] + 1
    return (info_loc[1], loc[2] + 1)

add_perf_lift

add_perf_lift(em, ws, m2_data, m1_data, info, perf_metrics, sample_prefices, wTitle=True)

Add Performance Lift.

Writes, from left to right starting at the cursor, a block of identifying information followed by one block per sample with the relative lift of model 2 over model 1: m2 / m1 - 1 for every performance metric, as percentages with data bars.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The blocks start at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
m2_data DataFrame

Performance table of model 2 (the numerator), with the columns named in info and <prefix><metric>. The column info[0] must hold strings.

required
m1_data DataFrame

Performance table of model 1, the baseline (the denominator), with the same layout. Its rows are matched with those of m2_data by index.

required
info list of str

Identifying columns, at least three: the first three become the (multi-level) index of the first block. The value of info[0] is written as "<value in m2_data> over <value in m1_data>".

required
perf_metrics list of str

Metric names without the sample prefix.

required
sample_prefices list of str

Column prefixes of the samples (for example ["train_", "test_"]). Each prefix gives one block, titled with the prefix in upper case without surrounding underscores.

required
wTitle bool

If True, write a title row over every block (a blank title over the information block); if False, write none.

True

Returns:

Type Description
list of int

[first_row, first_col, last_row, last_col] of the information block. Unlike add_scr_info and add_perf_metrics, which return a 2-tuple, this is the range of the first block, not the end position.

Notes

Sets em.gap_number to 0, so the blocks touch each other. After writing, the cursor is placed in the column of the information block, on the row directly below the blocks.

Source code in ExcelMaster/Template.py
def add_perf_lift(em, ws, m2_data, m1_data, info, perf_metrics, sample_prefices, wTitle = True):
    """Add Performance Lift.

    Writes, from left to right starting at the cursor, a block of identifying information followed by one block per sample
    with the relative lift of model 2 over model 1: ``m2 / m1 - 1`` for every performance metric, as percentages with
    data bars.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The blocks start at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    m2_data : pandas.DataFrame
        Performance table of model 2 (the numerator), with the columns named in ``info`` and ``<prefix><metric>``. The
        column ``info[0]`` must hold strings.
    m1_data : pandas.DataFrame
        Performance table of model 1, the baseline (the denominator), with the same layout. Its rows are matched with those
        of ``m2_data`` by index.
    info : list of str
        Identifying columns, at least three: the first three become the (multi-level) index of the first block. The value
        of ``info[0]`` is written as ``"<value in m2_data> over <value in m1_data>"``.
    perf_metrics : list of str
        Metric names without the sample prefix.
    sample_prefices : list of str
        Column prefixes of the samples (for example ``["train_", "test_"]``). Each prefix gives one block, titled with the
        prefix in upper case without surrounding underscores.
    wTitle : bool, default True
        If True, write a title row over every block (a blank title over the information block); if False, write none.

    Returns
    -------
    list of int
        ``[first_row, first_col, last_row, last_col]`` of the information block. Unlike ``add_scr_info`` and
        ``add_perf_metrics``, which return a 2-tuple, this is the range of the first block, not the end position.

    Notes
    -----
    Sets ``em.gap_number`` to 0, so the blocks touch each other. After writing, the cursor is placed in the column of the
    information block, on the row directly below the blocks.
    """
    em.gap_number = 0
    title=" "
    if not wTitle:
        title = None

    info_data = m2_data[info]
    info_data[info[0]] = m2_data[info[0]] + " over " + m1_data[info[0]]
    info_data = info_data.set_index(info[:3])
    info_loc = em.write_dataframe(ws, df=info_data, retCellRange="value", skipby="col", title=title, titleformat="", index = True)

    i = 0
    while i < len(sample_prefices):

        title=sample_prefices[i].upper().strip("_")
        if not wTitle:
            title = None

        m2_perf = m2_data[[sample_prefices[i] + x for x in perf_metrics]]
        m2_perf.columns = [x.replace(sample_prefices[i], "") for x in m2_perf.columns]

        m1_perf = m1_data[[sample_prefices[i] + x for x in perf_metrics]]
        m1_perf.columns = [x.replace(sample_prefices[i], "") for x in m1_perf.columns]

        perf_lift = (m2_perf/m1_perf) - 1

        loc = em.write_dataframe(ws, df=perf_lift, retCellRange="value", skipby="col", title=title)
        em.set_cell_format(ws, cell_range=loc, cformat="NUM%.2")
        em.set_data_bar(ws, cell_range=loc)
        i += 1

    em.curr_col = info_loc[1]
    em.curr_row = loc[2] + 1
    return info_loc

get_seg_perf_comparison_report

get_seg_perf_comparison_report(em, ws, m1_data, bad, sample_prefices, title='Performance Comparison', m2_data=None, perf_metrics=['ks', 'top10_cap', 'top20_cap', 'top40_cap', 'auc'], nbins=100)

Get Segment Perf Eval Report.

Writes a title, a note on the gains bins and then three groups of tables: score statistics (add_scr_info), performance metrics (add_perf_metrics) and, when m2_data is given, the performance lift of model 2 over model 1 (add_perf_lift). The rows of model 2 are written directly under those of model 1 in the same columns, without titles.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
m1_data str or DataFrame

Per-segment table of model 1: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by input_validation). It needs the columns <first prefix>scr_name, true_bad, seg_name and seg_info and, for every prefix of sample_prefices, <prefix>num_of_total, <prefix>num_of_<bad>, <prefix>rate_of_<bad>, <prefix>mean_score and <prefix><metric> for each metric of perf_metrics. Percentage text in the metric columns is converted to numbers.

required
bad str

Name of the bad class as it appears in the column names num_of_<bad> and rate_of_<bad> (lower-cased by the function), for example "bad".

required
sample_prefices list of str

Column prefixes of the samples, in lower case like the lower-cased column names (for example ["train_", "test_"]). The first prefix also prefixes the scr_name column.

required
title str

Title of the report, written as a merged heading (ORANGE_H4, three columns wide).

"Performance Comparison"
m2_data str or DataFrame or None

Table of a second model to compare, with the same layout as m1_data. None reports model 1 only.

None
perf_metrics list of str

Metric names without the sample prefix.

["ks", "top10_cap", "top20_cap", "top40_cap", "auc"]
nbins int

Only used in the note "(Gains in <nbins> bins is used.)" below the title; it changes no number in the report.

100

Returns:

Type Description
Worksheet

ws.

Notes

Leaves em.gap_number at 0 and the cursor under the last block. A DataFrame passed as m1_data or m2_data is modified in place (lower-cased column names, converted percentage text in the metric columns).

Source code in ExcelMaster/Template.py
def get_seg_perf_comparison_report(em, ws, m1_data, bad, sample_prefices, 
                                   title = "Performance Comparison",
                                   m2_data = None, 
                                   perf_metrics = ["ks", "top10_cap", "top20_cap", "top40_cap", "auc"], 
                                   nbins=100):
    """Get Segment Perf Eval Report.

    Writes a title, a note on the gains bins and then three groups of tables: score statistics (``add_scr_info``),
    performance metrics (``add_perf_metrics``) and, when ``m2_data`` is given, the performance lift of model 2 over model 1
    (``add_perf_lift``). The rows of model 2 are written directly under those of model 1 in the same columns, without
    titles.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    m1_data : str or pandas.DataFrame
        Per-segment table of model 1: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by
        ``input_validation``). It needs the columns ``<first prefix>scr_name``, ``true_bad``, ``seg_name`` and
        ``seg_info`` and, for every prefix of ``sample_prefices``, ``<prefix>num_of_total``, ``<prefix>num_of_<bad>``,
        ``<prefix>rate_of_<bad>``, ``<prefix>mean_score`` and ``<prefix><metric>`` for each metric of ``perf_metrics``.
        Percentage text in the metric columns is converted to numbers.
    bad : str
        Name of the bad class as it appears in the column names ``num_of_<bad>`` and ``rate_of_<bad>`` (lower-cased by the
        function), for example ``"bad"``.
    sample_prefices : list of str
        Column prefixes of the samples, in lower case like the lower-cased column names (for example
        ``["train_", "test_"]``). The first prefix also prefixes the ``scr_name`` column.
    title : str, default "Performance Comparison"
        Title of the report, written as a merged heading (``ORANGE_H4``, three columns wide).
    m2_data : str or pandas.DataFrame or None, default None
        Table of a second model to compare, with the same layout as ``m1_data``. None reports model 1 only.
    perf_metrics : list of str, default ["ks", "top10_cap", "top20_cap", "top40_cap", "auc"]
        Metric names without the sample prefix.
    nbins : int, default 100
        Only used in the note ``"(Gains in <nbins> bins is used.)"`` below the title; it changes no number in the report.

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    Leaves ``em.gap_number`` at 0 and the cursor under the last block. A DataFrame passed as ``m1_data`` or ``m2_data``
    is modified in place (lower-cased column names, converted percentage text in the metric columns).
    """

    metric_cols = [sample + metric for sample in sample_prefices for metric in perf_metrics]

    m1_data = input_validation(m1_data)
    m1_data = convert_perc_str_to_float(m1_data, metric_cols)

    em.gap_number = 0
    em.merge_col(ws, ncols=3, text = title, cformat = "ORANGE_H4")
    em.gap_number = 2
    em.write_text_content(ws, input_text=f"(Gains in {nbins} bins is used.) \n \n")

    info = [f"{sample_prefices[0]}scr_name", "true_bad", "seg_name", "seg_info"]
    scr_info = ["num_of_total", f"num_of_{bad.lower()}", f"rate_of_{bad.lower()}", "mean_score"]
    fnl_loc = add_scr_info(em, ws, m1_data, info, scr_info, sample_prefices)

    if m2_data is not None:
        m2_data = input_validation(m2_data)
        m2_data = convert_perc_str_to_float(m2_data, metric_cols)
        fnl_loc = add_scr_info(em, ws, m2_data, info, scr_info, sample_prefices, False)

    em.curr_col = fnl_loc[0]
    em.curr_row = fnl_loc[1] + 3

    fnl_loc = add_perf_metrics(em, ws, m1_data, info, perf_metrics, sample_prefices)

    if m2_data is not None:
        fnl_loc = add_perf_metrics(em, ws, m2_data, info, perf_metrics, sample_prefices, False)

    em.curr_col = fnl_loc[0]
    em.curr_row = fnl_loc[1] + 3

    if m2_data is not None:
        info_loc = add_perf_lift(em, ws, m2_data, m1_data, info, perf_metrics, sample_prefices)
    return ws

get_means_chart_report

get_means_chart_report(em, ws, means_rpt, by_class, varlist, class_name=None, stats_list=['N', 'NMISS', 'MIN', 'MEAN', 'MAX'], inc_miss_rate=True, varcol='variable', attr_info=None)

Plot Means Chart and Get Report.

Writes a "Means Chart by Attribute" heading and then, for every variable of varlist, an optional attribute information table followed by one block per statistic of stats_list: a heading and a chart of that statistic by class, side by side. With inc_miss_rate each chart combines columns for the statistic with a line for the missing rate on a secondary axis.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
means_rpt str or DataFrame

Descriptive statistics, one row per variable and class: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by input_validation). It needs the columns varcol, by_class and one column per statistic of stats_list; when inc_miss_rate is True and there is no missing_rate column, also n and nmiss. Only the rows whose varcol value is in varlist are used.

required
by_class str

Column that defines the classes on the x axis (for example a month); the rows are sorted by it in ascending order.

required
varlist list of str

Variables to report (values of varcol, case-sensitive), one block each. A tqdm progress bar is shown.

required
class_name str or None

Title of the x axis. None uses by_class.

None
stats_list list of str

Statistics to chart, one chart each. The column is found by the lower-cased name; headings and y axis titles show the name as given.

['N', 'NMISS', 'MIN', 'MEAN', 'MAX']
inc_miss_rate bool

If True, add the missing rate as a line on a secondary axis (0.00%). It is read from the missing_rate column or, if absent, computed as nmiss / (n + nmiss). If False, only column charts are drawn.

True
varcol str

Name of the variable column in means_rpt and attr_info.

"variable"
attr_info str or DataFrame or None

Attribute information (path or DataFrame, column names lower-cased). When given, the rows whose varcol equals the variable name in UPPER case are written as a table above the charts of that variable.

None

Returns:

Type Description
Worksheet

ws.

Notes

Sets em.gap_number to 2 (0 while the attribute information is written) and resets the cursor to (0, 0) at the end, so pass loc or call em.reset_curr_loc before the next write if the cursor position matters. A DataFrame passed as means_rpt gets its column names lower-cased in place.

Source code in ExcelMaster/Template.py
def get_means_chart_report(em, ws, means_rpt, by_class, varlist, class_name = None, 
                           stats_list = ['N', 'NMISS', 'MIN', 'MEAN', 'MAX'], inc_miss_rate = True,
                           varcol = "variable", attr_info = None):
    """Plot Means Chart and Get Report.

    Writes a "Means Chart by Attribute" heading and then, for every variable of ``varlist``, an optional attribute
    information table followed by one block per statistic of ``stats_list``: a heading and a chart of that statistic by
    class, side by side. With ``inc_miss_rate`` each chart combines columns for the statistic with a line for the missing
    rate on a secondary axis.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    means_rpt : str or pandas.DataFrame
        Descriptive statistics, one row per variable and class: a DataFrame or the path of a csv / sas7bdat file (column
        names are lower-cased by ``input_validation``). It needs the columns ``varcol``, ``by_class`` and one column per
        statistic of ``stats_list``; when ``inc_miss_rate`` is True and there is no ``missing_rate`` column, also ``n``
        and ``nmiss``. Only the rows whose ``varcol`` value is in ``varlist`` are used.
    by_class : str
        Column that defines the classes on the x axis (for example a month); the rows are sorted by it in ascending order.
    varlist : list of str
        Variables to report (values of ``varcol``, case-sensitive), one block each. A tqdm progress bar is shown.
    class_name : str or None, default None
        Title of the x axis. None uses ``by_class``.
    stats_list : list of str, default ['N', 'NMISS', 'MIN', 'MEAN', 'MAX']
        Statistics to chart, one chart each. The column is found by the lower-cased name; headings and y axis titles show
        the name as given.
    inc_miss_rate : bool, default True
        If True, add the missing rate as a line on a secondary axis (``0.00%``). It is read from the ``missing_rate``
        column or, if absent, computed as ``nmiss / (n + nmiss)``. If False, only column charts are drawn.
    varcol : str, default "variable"
        Name of the variable column in ``means_rpt`` and ``attr_info``.
    attr_info : str or pandas.DataFrame or None, default None
        Attribute information (path or DataFrame, column names lower-cased). When given, the rows whose ``varcol`` equals
        the variable name in UPPER case are written as a table above the charts of that variable.

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    Sets ``em.gap_number`` to 2 (0 while the attribute information is written) and resets the cursor to ``(0, 0)`` at the
    end, so pass ``loc`` or call ``em.reset_curr_loc`` before the next write if the cursor position matters. A DataFrame
    passed as ``means_rpt`` gets its column names lower-cased in place.
    """
    from tqdm import tqdm

    means_rpt = input_validation(means_rpt)

    if class_name is None:
        class_name = by_class

    means_rpt.columns = [x.lower() for x in means_rpt.columns]
    varcol = varcol.lower()
    means_rpt = means_rpt[means_rpt[varcol].isin(varlist)]

    if inc_miss_rate:
        if "missing_rate" not in means_rpt.columns:
            means_rpt["missing_rate"] = means_rpt["nmiss"]/(means_rpt["n"] + means_rpt["nmiss"])

    # em.reset_curr_loc()
    em.gap_number = 2
    em.merge_col(ws, ncols = 5, text="Means Chart by Attribute")

    for var in tqdm(varlist):
        """ By Attribute. """

        if attr_info is not None:
            em.gap_number = 0
            attr_info_table = input_validation(attr_info)
            singe_attr_info = attr_info_table.query(f"{varcol} == '{var.upper()}'")
            em.write_dataframe(ws, df = singe_attr_info, 
                               index = False, 
                               title =f"Information for Attribute {var.upper()}", 
                               skipby="row", retCellRange="value")

        em.gap_number = 2
        example = means_rpt[means_rpt[varcol] == var]
        chart_loc = {}
        for stat in stats_list:
            """ By Statistic. """
            em.write_text_content(worksheet=ws, input_text=f"{stat} ({var}) [[ORANGE_H3]] \n")
            example = example.sort_values([by_class.lower()], ascending = True)

            if inc_miss_rate:
                loc = em.write_duo_chart(ws, 
                                         df = example, 
                                         x = by_class.lower(), 
                                         y1_list=[stat.lower()], 
                                         y2_list = ["missing_rate"],
                                         title = f"{stat.upper()} for Attribute {var}", 
                                         chart_size=(20, 10), 
                                         c1_type="column", 
                                         c2_type="line", 
                                         y2_num_format = "0.00%",
                                         major_gridlines=False, 
                                         xy_axes_name=(class_name, stat, "Missing Rate (%)"), 
                                         skipby="col", 
                                         y1_axis_range=None,
                                         y2_axis_range=None,
                                         retCellRange="value")
            else:
                loc = em.write_chart(ws, 
                                     df = example, 
                                     x = by_class.lower(), 
                                     y_list=[stat.lower()], 
                                     title = f"{stat.upper()} for Attribute {var}", 
                                     chart_size=(20, 10), 
                                     chart_type="column", 
                                     major_gridlines=False, 
                                     xy_axes_name=(class_name, stat), 
                                     skipby="col", 
                                     y_axis_range=None,
                                     retCellRange="value")

            chart_loc[stat] = loc

            em.curr_row = loc[0] - 1

        chart1_loc = chart_loc[stats_list[0]]
        em.curr_row = chart1_loc[2] + em.gap_number
        em.curr_col = chart1_loc[1]

    em.reset_curr_loc()
    return ws

get_grid_boxplot_report

get_grid_boxplot_report(em, ws, perf_res, hparam_list, metric_list, fontsize=12, figsize=(30, 13), transp_bg=True, color_grp=(20, 1), colored_box=True)

Plot Boxplots of Grid Search Result for Hyperparams.

Writes a "Boxplot For Grid Search Result" heading and then, for every metric, a heading and one box plot image per hyperparameter, side by side: the distribution of the metric (shown as percent) over the values of the hyperparameter.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
perf_res DataFrame

Grid search results, one row per run, with the columns named in hparam_list and metric_list. It must be a DataFrame (a path is not accepted); the metrics are fractions, multiplied by 100 for display.

required
hparam_list list of str

Hyperparameter columns for the x axis, one plot each (one box per distinct value).

required
metric_list list of str

Metric columns to plot, one heading and one row of plots each.

required
fontsize int

Font size of the plot labels.

12
figsize tuple of int

Size of each inserted image as (rows, columns) of worksheet cells (see ExcelMaster.write_boxplot), not inches.

(30, 13)
transp_bg bool

Save the plot images with a transparent background.

True
color_grp tuple or str or list of str

Colors of the boxes, as accepted by color_input_validation: a (start_num, step) tuple that picks XKCD named colors, one color code for all boxes, or a list with one color code per box.

(20, 1)
colored_box bool

Not used: the call always passes colored_box=True to write_boxplot, so the boxes are always filled with colors.

True

Returns:

Type Description
Worksheet

ws.

Notes

Sets em.gap_number to 2. The plot images are temporary files (.tmp_image_*.png) in the working directory; they are deleted by em.close_workbook().

Source code in ExcelMaster/Template.py
def get_grid_boxplot_report(em, ws, perf_res, hparam_list, metric_list, fontsize = 12, figsize = (30, 13), transp_bg = True, color_grp = (20, 1), colored_box = True):
    """Plot Boxplots of Grid Search Result for Hyperparams.

    Writes a "Boxplot For Grid Search Result" heading and then, for every metric, a heading and one box plot image per
    hyperparameter, side by side: the distribution of the metric (shown as percent) over the values of the
    hyperparameter.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    perf_res : pandas.DataFrame
        Grid search results, one row per run, with the columns named in ``hparam_list`` and ``metric_list``. It must be a
        DataFrame (a path is not accepted); the metrics are fractions, multiplied by 100 for display.
    hparam_list : list of str
        Hyperparameter columns for the x axis, one plot each (one box per distinct value).
    metric_list : list of str
        Metric columns to plot, one heading and one row of plots each.
    fontsize : int, default 12
        Font size of the plot labels.
    figsize : tuple of int, default (30, 13)
        Size of each inserted image as ``(rows, columns)`` of worksheet cells (see ``ExcelMaster.write_boxplot``), not
        inches.
    transp_bg : bool, default True
        Save the plot images with a transparent background.
    color_grp : tuple or str or list of str, default (20, 1)
        Colors of the boxes, as accepted by ``color_input_validation``: a ``(start_num, step)`` tuple that picks XKCD
        named colors, one color code for all boxes, or a list with one color code per box.
    colored_box : bool, default True
        Not used: the call always passes ``colored_box=True`` to ``write_boxplot``, so the boxes are always filled with
        colors.

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    Sets ``em.gap_number`` to 2. The plot images are temporary files (``.tmp_image_*.png``) in the working directory; they
    are deleted by ``em.close_workbook()``.
    """

    em.gap_number = 2

    em.merge_col(ws, ncols = 5, text="Boxplot For Grid Search Result")

    chart_loc = {}
    for metric in metric_list:

        em.write_text_content(worksheet=ws, input_text=f"{metric} [[ORANGE_H3]] \n")

        for param in hparam_list:
            loc = em.write_boxplot(ws, 
                             df = perf_res[[param, metric]],
                             x = param,
                             y = metric,
                             y_percentage = True,
                             show_fig = False,
                             colored_box = True,
                             fontsize = fontsize,
                             figsize = figsize,
                             color_grp = color_grp,
                             title=  f"Boxplot for {string_proc(param)}",
                             transp_bg = transp_bg,
                             skipby = "col", 
                             retCellRange = "value")

            chart_loc[param] = loc

        chart1_loc = chart_loc[hparam_list[0]]
        em.curr_row = chart1_loc[2] + em.gap_number
        em.curr_col = chart1_loc[1]

    return ws

get_var_reduct_report

get_var_reduct_report(em, ws, vr_perf, metric_cols, target_metrics=None, basic_info_text=None, nvars_col='nvars')

Generate Variable Reduction Excel Report.

Writes a heading, an optional text, three tables side by side (model information, performance by metric and performance shift) and then one chart per target metric, two charts per row: the metric on the primary axis and its shift on the secondary axis, against the number of variables.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
vr_perf str or DataFrame

Variable reduction results, one row per model: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by input_validation). It needs the column nvars_col and the columns of metric_cols; every other column goes to the "Model Information" table. Percentage text in the metric columns is converted to numbers.

required
metric_cols list of str

Metric columns, in lower case, written in the performance table.

required
target_metrics list of str or None

Metrics for which the shift <metric>_shift (see get_metric_shift) is computed and a chart is drawn. None uses all of metric_cols.

None
basic_info_text str or None

Text written under the heading with write_text_content (its markup applies; end it with a newline). None writes nothing.

None
nvars_col str

Column with the number of variables, used as the x axis of the charts. The shift is always computed from the column named "nvars", so another name only works when a column nvars exists as well (KeyError otherwise).

"nvars"

Returns:

Type Description
Worksheet

ws.

Notes

A DataFrame passed as vr_perf is modified in place: its columns are lower-cased, percentage text is converted and the <metric>_shift columns are added. Sets em.gap_number to 2.

Source code in ExcelMaster/Template.py
def get_var_reduct_report(em, ws, vr_perf, metric_cols, target_metrics = None, basic_info_text = None, nvars_col = "nvars"):
    """Generate Variable Reduction Excel Report.

    Writes a heading, an optional text, three tables side by side (model information, performance by metric and
    performance shift) and then one chart per target metric, two charts per row: the metric on the primary axis and its
    shift on the secondary axis, against the number of variables.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    vr_perf : str or pandas.DataFrame
        Variable reduction results, one row per model: a DataFrame or the path of a csv / sas7bdat file (column names are
        lower-cased by ``input_validation``). It needs the column ``nvars_col`` and the columns of ``metric_cols``; every
        other column goes to the "Model Information" table. Percentage text in the metric columns is converted to
        numbers.
    metric_cols : list of str
        Metric columns, in lower case, written in the performance table.
    target_metrics : list of str or None, default None
        Metrics for which the shift ``<metric>_shift`` (see ``get_metric_shift``) is computed and a chart is drawn. None
        uses all of ``metric_cols``.
    basic_info_text : str or None, default None
        Text written under the heading with ``write_text_content`` (its markup applies; end it with a newline). None
        writes nothing.
    nvars_col : str, default "nvars"
        Column with the number of variables, used as the x axis of the charts. The shift is always computed from the column
        named ``"nvars"``, so another name only works when a column ``nvars`` exists as well (``KeyError`` otherwise).

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    A DataFrame passed as ``vr_perf`` is modified in place: its columns are lower-cased, percentage text is converted and
    the ``<metric>_shift`` columns are added. Sets ``em.gap_number`` to 2.
    """

    if target_metrics is None:
        target_metrics = metric_cols

    nvars_col = nvars_col.lower()

    vr_perf = input_validation(vr_perf)
    vr_perf = convert_perc_str_to_float(vr_perf, metric_cols)
    res = get_metrics_shift(vr_perf, [x for x in metric_cols if x in target_metrics])

    shift_cols = [y + "_shift" for y in metric_cols]

    all_metric_cols = metric_cols + shift_cols
    info_table = vr_perf[[x for x in vr_perf.columns if x not in all_metric_cols]]
    metrics_table = vr_perf[[x for x in vr_perf.columns if x in metric_cols]]
    shift_table = vr_perf[[x for x in vr_perf.columns if x in shift_cols]]

    em.write_text_content(ws, input_text="{#} Variable Reduction Report \n")

    if basic_info_text:
        em.write_text_content(ws, input_text=basic_info_text)

    em.gap_number = 0
    info_loc = em.write_dataframe(ws, df=info_table, title = "Model Information", header = True, index = False, retCellRange="value", skipby="col")
    perf_loc = em.write_dataframe(ws, df=metrics_table, title = "Variable Reduction Performance", header = True, index = False, retCellRange="value", skipby="col")
    shift_loc = em.write_dataframe(ws, df=shift_table, title = "Performance Shift", header = True, index = False, retCellRange="value", skipby="row")

    em.set_cell_format(ws, perf_loc, cformat="NUM%.2")
    em.set_cell_format(ws, shift_loc, cformat="NUM%.2")
    em.set_data_bar(ws, shift_loc)

    em.gap_number = 2
    chart_start_loc = (info_loc[2] + em.gap_number, info_loc[1])
    # print(chart_start_loc)
    em.reset_curr_loc(loc = chart_start_loc)

    i = 0
    chart_loc = []
    while i < len(target_metrics):

        metric = target_metrics[i]
        logger.info(metric)

        loc = em.write_duo_chart(worksheet = ws, 
                                 df = res, 
                                 y1_list = [metric], 
                                 y2_list = [metric + '_shift'], 
                                 x = nvars_col,
                                 c1_type = "line", 
                                 c2_type = "line", 
                                 y1_axis_range = None,
                                 y2_axis_range = None, 
                                 title = metric, 
                                 chart_size = (20, 10),
                                 xy_axes_name = ("Nvars", metric, metric + '_shift'),
                                 major_gridlines=False,
                                 retChart = False, 
                                 retCellRange = "value",
                                 skipby = "col",
                                 y1_num_format = "0.00%",
                                 y2_num_format = "0.00%")

        chart_loc.append(loc)
        # print(loc)

        if (i != 0) and (i % 2 == 1):
            reset_loc = (chart_loc[i][2] + em.gap_number, chart_loc[0][1])
            # print(reset_loc)
            em.reset_curr_loc(loc = reset_loc)

        i += 1
    # print(chart_loc)
    return ws

get_grid_search_report

get_grid_search_report(em, ws, rs_perf, metric_cols, sample_prefices, basic_info_text=None, sortby='hd_auc')

Get Grid Search Report.

Writes a heading, an optional text and three tables side by side: model information, performance by metric and the overfitting shift of the later samples relative to the first one.

Parameters:

Name Type Description Default
em ExcelMaster

Writer. The report starts at its current cursor.

required
ws Worksheet

Worksheet to write on.

required
rs_perf str or DataFrame

Grid (or random) search results, one row per model: a DataFrame or the path of a csv / sas7bdat file (column names are lower-cased by input_validation). Percentage text in the metric_cols columns is converted to numbers. The columns that are neither metrics nor shift columns form the "Model Information" table.

required
metric_cols list of str

Metric columns, in lower case, written in the performance table.

required
sample_prefices list of str

Column prefixes of the samples (for example ["bd_", "od_", "hd_"]). For every prefix after the first, compute_overfitting_shift adds the columns <column>_shift = metric of that sample / metric of the first sample - 1. Every column whose name ends with _shift goes to the "Overfitting Performance Shift" table.

required
basic_info_text str or None

Text written under the heading with write_text_content (its markup applies; end it with a newline). None writes nothing.

None
sortby str or None

Column by which the rows are sorted in descending order. None or an empty string keeps the input order.

"hd_auc"

Returns:

Type Description
Worksheet

ws.

Notes

Sets em.gap_number to 0. The metric and shift tables are formatted as percentages with 2 decimals and the shift table gets data bars. A DataFrame passed as rs_perf is modified in place: its column names are lower-cased and the percentage text of metric_cols is converted (the *_shift columns are added to it only when sortby is None, because sorting works on a copy).

Source code in ExcelMaster/Template.py
def get_grid_search_report(em, ws, rs_perf, metric_cols, sample_prefices, basic_info_text = None, sortby = "hd_auc"):
    """Get Grid Search Report.

    Writes a heading, an optional text and three tables side by side: model information, performance by metric and the
    overfitting shift of the later samples relative to the first one.

    Parameters
    ----------
    em : ExcelMaster
        Writer. The report starts at its current cursor.
    ws : xlsxwriter.worksheet.Worksheet
        Worksheet to write on.
    rs_perf : str or pandas.DataFrame
        Grid (or random) search results, one row per model: a DataFrame or the path of a csv / sas7bdat file (column names
        are lower-cased by ``input_validation``). Percentage text in the ``metric_cols`` columns is converted to numbers.
        The columns that are neither metrics nor shift columns form the "Model Information" table.
    metric_cols : list of str
        Metric columns, in lower case, written in the performance table.
    sample_prefices : list of str
        Column prefixes of the samples (for example ``["bd_", "od_", "hd_"]``). For every prefix after the first,
        ``compute_overfitting_shift`` adds the columns ``<column>_shift`` = metric of that sample / metric of the first
        sample - 1. Every column whose name ends with ``_shift`` goes to the "Overfitting Performance Shift" table.
    basic_info_text : str or None, default None
        Text written under the heading with ``write_text_content`` (its markup applies; end it with a newline). None
        writes nothing.
    sortby : str or None, default "hd_auc"
        Column by which the rows are sorted in descending order. None or an empty string keeps the input order.

    Returns
    -------
    xlsxwriter.worksheet.Worksheet
        ``ws``.

    Notes
    -----
    Sets ``em.gap_number`` to 0. The metric and shift tables are formatted as percentages with 2 decimals and the shift
    table gets data bars. A DataFrame passed as ``rs_perf`` is modified in place: its column names are lower-cased and the
    percentage text of ``metric_cols`` is converted (the ``*_shift`` columns are added to it only when ``sortby`` is
    None, because sorting works on a copy).
    """
    rs_perf = input_validation(rs_perf)
    rs_perf = convert_perc_str_to_float(rs_perf, metric_cols)

    if sortby:
        rs_perf = rs_perf.sort_values([sortby], ascending = False)

    i = 1
    while i <= len(sample_prefices) - 1:
        rs_perf = compute_overfitting_shift(rs_perf, (sample_prefices[0], sample_prefices[i]))
        i += 1

    shift_cols = [x for x in rs_perf.columns if x.endswith("_shift")]

    all_metric_cols = metric_cols + shift_cols
    info_table = rs_perf[[x for x in rs_perf.columns if x not in all_metric_cols]]
    metrics_table = rs_perf[[x for x in rs_perf.columns if x in metric_cols]]
    shift_table = rs_perf[[x for x in rs_perf.columns if x in shift_cols]]

    em.write_text_content(ws, input_text="{#} Grid Search Report \n")

    if basic_info_text:
            em.write_text_content(ws, input_text=basic_info_text)

    em.gap_number = 0
    info_loc = em.write_dataframe(ws, df=info_table, title = "Model Information", header = True, index = False, retCellRange="value", skipby="col")
    perf_loc = em.write_dataframe(ws, df=metrics_table, title = "Grid Search Performance", header = True, index = False, retCellRange="value", skipby="col")
    shift_loc = em.write_dataframe(ws, df=shift_table, title = "Overfitting Performance Shift", header = True, index = False, retCellRange="value", skipby="row")

    em.set_cell_format(ws, perf_loc, cformat="NUM%.2")
    em.set_cell_format(ws, shift_loc, cformat="NUM%.2")
    em.set_data_bar(ws, shift_loc)

    return ws

Utilities: Utility

Utility

getStartDateofLatestWeek

getStartDateofLatestWeek(retStr=True)

Date of first day of last week.

Returns the Sunday that immediately precedes the Monday starting the current week (weeks start on Monday and are counted with %W). Every day from Monday to Sunday of one week gives the same result, for example 2026-09-27 for 2026-09-28 through 2026-10-04.

Parameters:

Name Type Description Default
retStr bool

If True, return the date as a "%Y-%m-%d" string; otherwise return a datetime.datetime at midnight.

True

Returns:

Type Description
str or datetime

The date described above.

Source code in ExcelMaster/Utility.py
def getStartDateofLatestWeek(retStr=True):
    """Date of first day of last week.

    Returns the Sunday that immediately precedes the Monday starting the current week (weeks start on Monday and are
    counted with ``%W``). Every day from Monday to Sunday of one week gives the same result, for example 2026-09-27 for
    2026-09-28 through 2026-10-04.

    Parameters
    ----------
    retStr : bool, default True
        If True, return the date as a ``"%Y-%m-%d"`` string; otherwise return a ``datetime.datetime`` at midnight.

    Returns
    -------
    str or datetime.datetime
        The date described above.
    """
    import datetime
    today = datetime.date.today()
    curr_wk = today.strftime("%W")
    d = f"{str(today.year)}-W{curr_wk}"
    r = datetime.datetime.strptime(d + '-1', "%Y-W%W-%w")
    res = (r + datetime.timedelta(days=-1))
    if retStr:
        return res.strftime("%Y-%m-%d")
    return res

getLastCompletedVintage

getLastCompletedVintage(start_date=None, format='%Y-%m-%d', vintage=False)

Last Completed Vintage

Returns the last day of the month that precedes the month of start_date (or of today), that is the most recent completed month.

Parameters:

Name Type Description Default
start_date str or None

Reference date as text in format. None uses today's date.

None
format str

strptime / strftime format used to parse start_date and to format the result.

"%Y-%m-%d"
vintage bool

If True, return the month as a YYYYMM integer (for example 202609) instead of the formatted date. The digits are cut from the first 7 characters of the formatted date, so this is only correct when format starts with %Y-%m (the default does).

False

Returns:

Type Description
str or int

The last day of the previous month formatted with format (for example "2026-09-30"), or the YYYYMM integer when vintage is True.

Source code in ExcelMaster/Utility.py
def getLastCompletedVintage(start_date = None, format="%Y-%m-%d", vintage=False):
    """Last Completed Vintage

    Returns the last day of the month that precedes the month of ``start_date`` (or of today), that is the most recent
    completed month.

    Parameters
    ----------
    start_date : str or None, default None
        Reference date as text in ``format``. None uses today's date.
    format : str, default "%Y-%m-%d"
        ``strptime`` / ``strftime`` format used to parse ``start_date`` and to format the result.
    vintage : bool, default False
        If True, return the month as a ``YYYYMM`` integer (for example ``202609``) instead of the formatted date. The digits
        are cut from the first 7 characters of the formatted date, so this is only correct when ``format`` starts with
        ``%Y-%m`` (the default does).

    Returns
    -------
    str or int
        The last day of the previous month formatted with ``format`` (for example ``"2026-09-30"``), or the ``YYYYMM``
        integer when ``vintage`` is True.
    """
    import datetime

    todayDate = datetime.date.today()

    if start_date is not None:
        todayDate = datetime.datetime.strptime(start_date, format).date()    

    lastM = todayDate.replace(day=1) - datetime.timedelta(days=1)
    if vintage:
        return int(lastM.strftime(format)[0:7].replace("-",""))
    return lastM.strftime(format)

vin2quar

vin2quar(strDate)

String Vintage to Quarter (if completed month).

Parameters:

Name Type Description Default
strDate str

Vintage as "YYYYMM" text; only the first six characters are read.

required

Returns:

Type Description
str

"YYYYQn" (for example "2026Q2" for "202606") when the month is the last month of a quarter (3, 6, 9 or 12); otherwise strDate unchanged.

Source code in ExcelMaster/Utility.py
def vin2quar(strDate):
    """String Vintage to Quarter (if completed month).

    Parameters
    ----------
    strDate : str
        Vintage as ``"YYYYMM"`` text; only the first six characters are read.

    Returns
    -------
    str
        ``"YYYYQn"`` (for example ``"2026Q2"`` for ``"202606"``) when the month is the last month of a quarter (3, 6, 9 or
        12); otherwise ``strDate`` unchanged.
    """
    year = int(strDate[:4])
    month = int(strDate[4:6])
    completed_q = [3, 6, 9, 12]
    if month in completed_q:
        q = (month-1)//3 + 1
        return str(year) + "Q" + str(q)
    return strDate

list_files

list_files(location, pattern)

List all files.

Parameters:

Name Type Description Default
location str

Directory that is searched recursively (os.walk).

required
pattern str

Regular expression searched (re.search) in each file name.

required

Returns:

Type Description
list of str

Bare file names (without their directory) of the matching files in walk order; a name that exists in several directories appears once per directory.

Source code in ExcelMaster/Utility.py
def list_files(location, pattern):
    """List all files.

    Parameters
    ----------
    location : str
        Directory that is searched recursively (``os.walk``).
    pattern : str
        Regular expression searched (``re.search``) in each file name.

    Returns
    -------
    list of str
        Bare file names (without their directory) of the matching files in walk order; a name that exists in several
        directories appears once per directory.
    """
    import re
    res = []
    for root, dirs, files in os.walk(location):
        for file in files:
            if re.search(pattern, file):
                 res.append(file)
    return res

getCurrentDateTime

getCurrentDateTime(fmt='%Y%m%d%H%M%S')

Get Current DateTime

Parameters:

Name Type Description Default
fmt str

strftime format of the result.

"%Y%m%d%H%M%S"

Returns:

Type Description
str

Current local date and time formatted with fmt.

Source code in ExcelMaster/Utility.py
def getCurrentDateTime(fmt = "%Y%m%d%H%M%S"):
    """Get Current DateTime

    Parameters
    ----------
    fmt : str, default "%Y%m%d%H%M%S"
        ``strftime`` format of the result.

    Returns
    -------
    str
        Current local date and time formatted with ``fmt``.
    """
    import datetime
    return datetime.datetime.now().strftime(fmt)

input_table_proc

input_table_proc(tbl)

Process Input Table.

Lower-cases the column names of a table.

Parameters:

Name Type Description Default
tbl DataFrame

Table with string column names.

required

Returns:

Type Description
DataFrame

The same object with lower-cased column names; the caller's DataFrame is renamed in place.

Source code in ExcelMaster/Utility.py
def input_table_proc(tbl):
    """Process Input Table.

    Lower-cases the column names of a table.

    Parameters
    ----------
    tbl : pandas.DataFrame
        Table with string column names.

    Returns
    -------
    pandas.DataFrame
        The same object with lower-cased column names; the caller's DataFrame is renamed in place.
    """
    tbl.columns = [x.lower() for x in tbl.columns]
    return tbl

get_file_extension

get_file_extension(input_path)

Get File Extentsion for a given file Path.

Parameters:

Name Type Description Default
input_path str

File path or name.

required

Returns:

Type Description
str

The extension including the leading dot, in its original case (".csv"), or "" when there is none.

Source code in ExcelMaster/Utility.py
def get_file_extension(input_path):
    """Get File Extentsion for a given file Path.

    Parameters
    ----------
    input_path : str
        File path or name.

    Returns
    -------
    str
        The extension including the leading dot, in its original case (``".csv"``), or ``""`` when there is none.
    """
    return os.path.splitext(input_path)[1]

input_validation

input_validation(x, sep=',')

Input Validation.

Loads a table from a file path, or accepts a DataFrame, and lower-cases its column names.

Parameters:

Name Type Description Default
x str or DataFrame

A DataFrame, or the path of a file: a .sas7bdat file (the extension is compared case-sensitively) is read with pd.read_sas(encoding="latin-1"), any other file is read as delimited text with pd.read_csv.

required
sep str

Field delimiter of the text file; ignored for .sas7bdat files and DataFrames.

","

Returns:

Type Description
DataFrame

The table with lower-cased column names. A DataFrame passed in is returned as the same object, so its columns are renamed in place.

Raises:

Type Description
AttributeError

If x is neither a string nor a DataFrame (the exception type is AttributeError, not TypeError).

Source code in ExcelMaster/Utility.py
def input_validation(x, sep=","):
    """Input Validation.

    Loads a table from a file path, or accepts a DataFrame, and lower-cases its column names.

    Parameters
    ----------
    x : str or pandas.DataFrame
        A DataFrame, or the path of a file: a ``.sas7bdat`` file (the extension is compared case-sensitively) is read
        with ``pd.read_sas(encoding="latin-1")``, any other file is read as delimited text with ``pd.read_csv``.
    sep : str, default ","
        Field delimiter of the text file; ignored for ``.sas7bdat`` files and DataFrames.

    Returns
    -------
    pandas.DataFrame
        The table with lower-cased column names. A DataFrame passed in is returned as the same object, so its columns
        are renamed in place.

    Raises
    ------
    AttributeError
        If ``x`` is neither a string nor a DataFrame (the exception type is AttributeError, not TypeError).
    """
    import os
    if isinstance(x, str):
        if get_file_extension(x) == ".sas7bdat":
            res = pd.read_sas(x, encoding="latin-1")
        else:
            res = pd.read_csv(x, sep = sep)
        res = input_table_proc(res)
        return res
    elif isinstance(x, pd.DataFrame):
        return input_table_proc(x)
    else:
        raise AttributeError("Only Support csv/sas7bdat Path or Panda DataFrame as Input!!!")

val_input_condition

val_input_condition(target, condition=('>', 20))

Condition Tuple Validation.

Parameters:

Name Type Description Default
target float or str

Value to test.

required
condition tuple or str

A string tests equality: target == condition. A (operator, value) tuple compares target with float(value); the operator (case-insensitive, surrounding blanks ignored) is one of ">" / "gt", "<" / "lt", "=" / "eq", ">=" / "gte", "<=" / "lte".

(">", 20)

Returns:

Type Description
bool or None

The result of the test. None (falsy) for an operator that is not in the list above, for example "==" or "!=".

Source code in ExcelMaster/Utility.py
def val_input_condition(target, condition = (">", 20)):
    """Condition Tuple Validation.

    Parameters
    ----------
    target : float or str
        Value to test.
    condition : tuple or str, default (">", 20)
        A string tests equality: ``target == condition``. A ``(operator, value)`` tuple compares ``target`` with
        ``float(value)``; the operator (case-insensitive, surrounding blanks ignored) is one of ``">"`` / ``"gt"``,
        ``"<"`` / ``"lt"``, ``"="`` / ``"eq"``, ``">="`` / ``"gte"``, ``"<="`` / ``"lte"``.

    Returns
    -------
    bool or None
        The result of the test. None (falsy) for an operator that is not in the list above, for example ``"=="`` or
        ``"!="``.
    """
    if isinstance(condition, str):
        return (target == condition)

    else:
        operator = condition[0].strip().lower()
        value = float(condition[1])

        if operator == '>' or operator == 'gt':
            return (target > value)
        elif operator == '<' or operator == 'lt':
            return (target < value)
        elif operator == '=' or operator == 'eq':
            return (target == value)
        elif operator == '>=' or operator == 'gte':
            return (target >= value)
        elif operator == '<=' or operator == 'lte':
            return (target <= value)
        elif operator == '=' or operator == 'eq':
            return (target == value)

get_quarter

get_quarter(strDate)

Get the quarter number of a vintage.

Parameters:

Name Type Description Default
strDate str

Vintage as "YYYYMM" text; characters 5 and 6 are read as the month.

required

Returns:

Type Description
int

Quarter of the month, 1 to 4 (for example 2 for "202605").

Source code in ExcelMaster/Utility.py
def get_quarter(strDate):
    """Get the quarter number of a vintage.

    Parameters
    ----------
    strDate : str
        Vintage as ``"YYYYMM"`` text; characters 5 and 6 are read as the month.

    Returns
    -------
    int
        Quarter of the month, 1 to 4 (for example ``2`` for ``"202605"``).
    """
    return ((int(strDate[4:6])-1)//3) + 1

tanspose_dataframe

tanspose_dataframe(df, index_col)

Transpose Pandas DataFrame.

The spelling of the function name is historical.

Parameters:

Name Type Description Default
df DataFrame

Table to transpose.

required
index_col str or list of str

Column(s) set as the index before the transposition; their values become the column labels of the result.

required

Returns:

Type Description
DataFrame

df.set_index(index_col).T.reset_index(): the former column names are in a leading column named index.

Source code in ExcelMaster/Utility.py
def tanspose_dataframe(df, index_col):
    """Transpose Pandas DataFrame.

    The spelling of the function name is historical.

    Parameters
    ----------
    df : pandas.DataFrame
        Table to transpose.
    index_col : str or list of str
        Column(s) set as the index before the transposition; their values become the column labels of the result.

    Returns
    -------
    pandas.DataFrame
        ``df.set_index(index_col).T.reset_index()``: the former column names are in a leading column named ``index``.
    """
    df = df.set_index(index_col).T.reset_index()
    return df

convert_perc_str_to_float

convert_perc_str_to_float(df, cols)

Percentage to Float.

Parameters:

Name Type Description Default
df DataFrame

Table to convert.

required
cols list of str

Columns to convert. A column is converted only when its dtype is object (percentage text such as "12.5%"): the trailing % is removed and the number is divided by 100. Columns of any other dtype are left unchanged.

required

Returns:

Type Description
DataFrame

df itself; the columns are replaced in place.

Source code in ExcelMaster/Utility.py
def convert_perc_str_to_float(df, cols):
    """Percentage to Float.

    Parameters
    ----------
    df : pandas.DataFrame
        Table to convert.
    cols : list of str
        Columns to convert. A column is converted only when its dtype is ``object`` (percentage text such as
        ``"12.5%"``): the trailing ``%`` is removed and the number is divided by 100. Columns of any other dtype are left
        unchanged.

    Returns
    -------
    pandas.DataFrame
        ``df`` itself; the columns are replaced in place.
    """
    for col in cols:
        if str(df[col].dtypes) == 'object':
            df[col] = df[col].str.rstrip('%').astype('float') / 100
    return df

color_hex2rgb

color_hex2rgb(hex_code)

Convert Color Hex Code to RGB Tuple.

Parameters:

Name Type Description Default
hex_code str

Color as six hexadecimal digits with an optional leading # ("#FF8800" or "ff8800"). The three-digit shorthand is not supported.

required

Returns:

Type Description
tuple of int

(red, green, blue) with values from 0 to 255, for example (255, 136, 0).

Source code in ExcelMaster/Utility.py
def color_hex2rgb(hex_code):
    """Convert Color Hex Code to RGB Tuple.

    Parameters
    ----------
    hex_code : str
        Color as six hexadecimal digits with an optional leading ``#`` (``"#FF8800"`` or ``"ff8800"``). The three-digit
        shorthand is not supported.

    Returns
    -------
    tuple of int
        ``(red, green, blue)`` with values from 0 to 255, for example ``(255, 136, 0)``.
    """
    hex_code = hex_code.lower()
    h = hex_code.lstrip('#')
    return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))

get_color_set

get_color_set(n, start_num=0, step=1, retName=False, lookupName=None)

Return a set of color code without replacement.

Picks colors from the XKCD color table of matplotlib (949 named colors).

Parameters:

Name Type Description Default
n int

Number of colors.

required
start_num int

Index in the XKCD table of the first color.

0
step int

Distance between two consecutive picked indices (start_num, start_num + step, ...). An index beyond the end of the table raises IndexError.

1
retName bool

If True, return a dict {color name: hex code} instead of a list of hex codes.

False
lookupName str or None

Regular expression that replaces the index-based picks: the first n colors whose table key matches it (re.search) are returned, fewer if fewer match. The key carries the "xkcd:" prefix ("xkcd:blue"), so the prefix is part of the searched text. start_num and step are then not used, but the index-based loop still runs first and can raise IndexError.

None

Returns:

Type Description
list of str or dict

Hex color codes ("#acc2d9"), or {name: code} when retName is True.

Source code in ExcelMaster/Utility.py
def get_color_set(n, start_num = 0, step = 1, retName = False, lookupName = None):
    """Return a set of color code without replacement.

    Picks colors from the XKCD color table of matplotlib (949 named colors).

    Parameters
    ----------
    n : int
        Number of colors.
    start_num : int, default 0
        Index in the XKCD table of the first color.
    step : int, default 1
        Distance between two consecutive picked indices (``start_num``, ``start_num + step``, ...). An index beyond the
        end of the table raises ``IndexError``.
    retName : bool, default False
        If True, return a dict ``{color name: hex code}`` instead of a list of hex codes.
    lookupName : str or None, default None
        Regular expression that replaces the index-based picks: the first ``n`` colors whose table key matches it
        (``re.search``) are returned, fewer if fewer match. The key carries the ``"xkcd:"`` prefix (``"xkcd:blue"``), so
        the prefix is part of the searched text. ``start_num`` and ``step`` are then not used, but the index-based loop still
        runs first and can raise ``IndexError``.

    Returns
    -------
    list of str or dict
        Hex color codes (``"#acc2d9"``), or ``{name: code}`` when ``retName`` is True.
    """
    import matplotlib.colors as mcolors
    import re

    colors = list(mcolors.XKCD_COLORS.items())
    color_set = {}

    for i in range(start_num, start_num + n * step, step):
        name = colors[i][0].replace("xkcd:", "")
        color = colors[i][1]
        color_set[name] = color

    if lookupName:
        color_set = {}
        i = 0
        for name, color in colors:
            if len(color_set.items()) == n:
                break
            if re.search(lookupName, name):
                color_set[name.replace("xkcd:", "")] = color
            i += 1

    if retName:
        return color_set
    return list(color_set.values())

string_proc

string_proc(x)

Process a given String.

Parameters:

Name Type Description Default
x str

Text whose words are separated by underscores, for example a column name.

required

Returns:

Type Description
str

The words capitalized (first letter upper-case, the rest lower-case) and joined by single spaces, for example "credit_SCORE_v2" becomes "Credit Score V2".

Source code in ExcelMaster/Utility.py
def string_proc(x):
    """Process a given String.

    Parameters
    ----------
    x : str
        Text whose words are separated by underscores, for example a column name.

    Returns
    -------
    str
        The words capitalized (first letter upper-case, the rest lower-case) and joined by single spaces, for example
        ``"credit_SCORE_v2"`` becomes ``"Credit Score V2"``.
    """
    x_list = x.split("_")
    x_list = [x.strip().capitalize() for x in x_list]
    x = " ".join(x_list)
    return x

convert_to_boxplot_data

convert_to_boxplot_data(df, x, y, y_percentage=False)

Convert dataframe to boxplot data.

Parameters:

Name Type Description Default
df DataFrame

Source table.

required
x str

Column that defines the groups (one box per distinct value).

required
y str

Column with the values of each group.

required
y_percentage bool

If True, multiply every value by 100.

False

Returns:

Type Description
dict

{group value: list of y values}, with the groups in ascending order of x.

Source code in ExcelMaster/Utility.py
def convert_to_boxplot_data(df, x, y, y_percentage = False):
    """Convert dataframe to boxplot data.

    Parameters
    ----------
    df : pandas.DataFrame
        Source table.
    x : str
        Column that defines the groups (one box per distinct value).
    y : str
        Column with the values of each group.
    y_percentage : bool, default False
        If True, multiply every value by 100.

    Returns
    -------
    dict
        ``{group value: list of y values}``, with the groups in ascending order of ``x``.
    """
    x_unique_value = df[x].sort_values().unique().tolist()

    box_plot_data = {}
    for v in x_unique_value:
        box_plot_data[v] = [num * 100 if y_percentage else num for num in df[df[x] == v][y].tolist()]
    return box_plot_data

color_input_validation

color_input_validation(color_grp, val_n)

Perform Color Input Validation.

Parameters:

Name Type Description Default
color_grp tuple or str or list of str

A 2-tuple (start_num, step) picks val_n named colors from the XKCD table with get_color_set; a string is one color code used for all boxes; a list of strings is used as it is and must have exactly val_n entries.

required
val_n int

Number of colors needed (number of boxes).

required

Returns:

Type Description
list of str

Color codes, one per box.

Raises:

Type Description
ValueError

If color_grp has none of the three accepted forms (this includes a list whose length is not val_n).

Source code in ExcelMaster/Utility.py
def color_input_validation(color_grp, val_n):
    """Perform Color Input Validation.

    Parameters
    ----------
    color_grp : tuple or str or list of str
        A 2-tuple ``(start_num, step)`` picks ``val_n`` named colors from the XKCD table with ``get_color_set``; a string is
        one color code used for all boxes; a list of strings is used as it is and must have exactly ``val_n`` entries.
    val_n : int
        Number of colors needed (number of boxes).

    Returns
    -------
    list of str
        Color codes, one per box.

    Raises
    ------
    ValueError
        If ``color_grp`` has none of the three accepted forms (this includes a list whose length is not ``val_n``).
    """
    if isinstance(color_grp, tuple) and len(color_grp) == 2:
        # Customize colors
        cols = get_color_set(val_n, color_grp[0], color_grp[1], False)
        colors = cols
    elif isinstance(color_grp, str):
        colors = [color_grp] * val_n
    elif (isinstance(color_grp, list)) and (all([isinstance(x, str) for x in color_grp])) and (len(color_grp) == val_n):
        colors = color_grp
    else:
        raise ValueError("Please give valid color_grp: tuple of two numbers, list of color code or a single color code.")
    return colors

get_metric_shift

get_metric_shift(data, metric_name, nvars_col='nvars')

Calculate Metric Shift for Variable Reduction.

For every row, the slope between consecutive rows: (metric - metric of the previous row) / (nvars - nvars of the previous row), that is the change of the metric per variable added (or removed, when the rows are ordered by decreasing number of variables).

Parameters:

Name Type Description Default
data DataFrame

Table with one row per model, ordered by the number of variables.

required
metric_name str

Column holding the metric.

required
nvars_col str

Column holding the number of variables.

"nvars"

Returns:

Type Description
Series

The shift of every row, with the index of data. The first row is 0 (missing values are filled with 0). Rows whose number of variables equals that of the previous row divide by zero: inf / -inf, or 0 when the metric is unchanged too.

Source code in ExcelMaster/Utility.py
def get_metric_shift(data, metric_name, nvars_col = "nvars"):
    """Calculate Metric Shift for Variable Reduction.

    For every row, the slope between consecutive rows: ``(metric - metric of the previous row) / (nvars - nvars of the
    previous row)``, that is the change of the metric per variable added (or removed, when the rows are ordered by
    decreasing number of variables).

    Parameters
    ----------
    data : pandas.DataFrame
        Table with one row per model, ordered by the number of variables.
    metric_name : str
        Column holding the metric.
    nvars_col : str, default "nvars"
        Column holding the number of variables.

    Returns
    -------
    pandas.Series
        The shift of every row, with the index of ``data``. The first row is 0 (missing values are filled with 0). Rows
        whose number of variables equals that of the previous row divide by zero: ``inf`` / ``-inf``, or 0 when the metric
        is unchanged too.
    """
    metric_lift = (data[metric_name] - data[metric_name].shift(1))
    nvars_reduced = (data[nvars_col] - data[nvars_col].shift(1))
    return (metric_lift / nvars_reduced).fillna(0)

get_metrics_shift

get_metrics_shift(data, metric_cols)

Get Shift for List of Metrics.

Adds a column <metric>_shift for every metric, computed with get_metric_shift and its default nvars_col="nvars", so data needs a column named nvars.

Parameters:

Name Type Description Default
data DataFrame

Table with one row per model, ordered by the number of variables.

required
metric_cols list of str

Metric columns.

required

Returns:

Type Description
DataFrame

data itself with the added *_shift columns (modified in place).

Source code in ExcelMaster/Utility.py
def get_metrics_shift(data, metric_cols):
    """Get Shift for List of Metrics.

    Adds a column ``<metric>_shift`` for every metric, computed with ``get_metric_shift`` and its default
    ``nvars_col="nvars"``, so ``data`` needs a column named ``nvars``.

    Parameters
    ----------
    data : pandas.DataFrame
        Table with one row per model, ordered by the number of variables.
    metric_cols : list of str
        Metric columns.

    Returns
    -------
    pandas.DataFrame
        ``data`` itself with the added ``*_shift`` columns (modified in place).
    """
    for metric in metric_cols:
        if metric.startswith(tuple(metric_cols)):
            data[metric+"_shift"] = get_metric_shift(data, metric)
    return data

compute_overfitting_shift

compute_overfitting_shift(data, sample_prefix)

Calculate Overfitting Performance Shift.

Parameters:

Name Type Description Default
data DataFrame

Table with the metric columns of both samples, named <prefix><metric>.

required
sample_prefix tuple of str

(base_prefix, other_prefix). The columns whose names start with sample_prefix[0] and those that start with sample_prefix[1] are paired in column order (not by name).

required

Returns:

Type Description
DataFrame

data itself (modified in place) with one added column <other column>_shift per pair, equal to other / base - 1: the relative change of the second sample versus the first.

Raises:

Type Description
ValueError

If the two prefixes select a different number of columns.

Source code in ExcelMaster/Utility.py
def compute_overfitting_shift(data, sample_prefix):
    """Calculate Overfitting Performance Shift.

    Parameters
    ----------
    data : pandas.DataFrame
        Table with the metric columns of both samples, named ``<prefix><metric>``.
    sample_prefix : tuple of str
        ``(base_prefix, other_prefix)``. The columns whose names start with ``sample_prefix[0]`` and those that start with
        ``sample_prefix[1]`` are paired in column order (not by name).

    Returns
    -------
    pandas.DataFrame
        ``data`` itself (modified in place) with one added column ``<other column>_shift`` per pair, equal to
        ``other / base - 1``: the relative change of the second sample versus the first.

    Raises
    ------
    ValueError
        If the two prefixes select a different number of columns.
    """
    b_metrics = [x for x in data.columns if x.startswith(sample_prefix[0])]
    o_metrics = [x for x in data.columns if x.startswith(sample_prefix[1])]

    if len(b_metrics) == len(o_metrics):
        for b_metric, o_metric in zip(b_metrics, o_metrics):
            data[o_metric+"_shift"] = data[o_metric].div(data[b_metric]) - 1
        return data

    raise ValueError("The lengths of metrics between two samples are not the same in the given dataset!")

proc_psi_raw_report

proc_psi_raw_report(psi_raw_table, psi_title, keep_list=None, varname='variable', upper=True)

Processing Raw PSI Report generated from Takecopter.

Parameters:

Name Type Description Default
psi_raw_table str or DataFrame

Raw PSI table: a DataFrame or the path of a csv / sas7bdat file, read with input_validation (so the column names are lower-cased). It must contain the column var_for_psi.

required
psi_title str

Title placed on the top level of the two-level column header of the result.

required
keep_list list of str or None

Columns to keep, spelled as in the processed table (upper-case when upper is True). None or an empty list keeps all columns.

None
varname str

New name of the var_for_psi column, which becomes the index.

"variable"
upper bool

If True, upper-case the names of the remaining columns.

True

Returns:

Type Description
DataFrame

Table indexed by varname whose columns are the two-level pairs (psi_title, column name).

Source code in ExcelMaster/Utility.py
def proc_psi_raw_report(psi_raw_table, psi_title, keep_list = None, varname = "variable", upper=True):
    """Processing Raw PSI Report generated from Takecopter.

    Parameters
    ----------
    psi_raw_table : str or pandas.DataFrame
        Raw PSI table: a DataFrame or the path of a csv / sas7bdat file, read with ``input_validation`` (so the column
        names are lower-cased). It must contain the column ``var_for_psi``.
    psi_title : str
        Title placed on the top level of the two-level column header of the result.
    keep_list : list of str or None, default None
        Columns to keep, spelled as in the processed table (upper-case when ``upper`` is True). None or an empty list keeps
        all columns.
    varname : str, default "variable"
        New name of the ``var_for_psi`` column, which becomes the index.
    upper : bool, default True
        If True, upper-case the names of the remaining columns.

    Returns
    -------
    pandas.DataFrame
        Table indexed by ``varname`` whose columns are the two-level pairs ``(psi_title, column name)``.
    """
    psi_table = input_validation(psi_raw_table)
    psi_table = psi_table.rename(columns={"var_for_psi":varname})
    psi_table = psi_table.set_index(varname)
    if upper:
        psi_table.columns = [x.upper() for x in psi_table.columns]
    psi_table.columns = [[psi_title]*len(psi_table.columns),psi_table.columns]
    if keep_list:
        psi_table = psi_table[[(psi_title, x) for x in keep_list]]
    return psi_table

get_mean_risk

get_mean_risk(bivar_single_attr, value_range_col=['min_indep', 'max_indep'], dep_col='dep')

get average risk

Parameters:

Name Type Description Default
bivar_single_attr DataFrame

Bivariate table of one attribute with one row per bin.

required
value_range_col list of str

Columns with the bounds of each bin. A row whose bounds are all missing is the missing-value bin and is left out of mean_no_nan.

['min_indep', 'max_indep']
dep_col str

Column with the bad rate of each bin.

"dep"

Returns:

Type Description
DataFrame

bivar_single_attr itself with two constant columns added in place: mean (mean of dep_col over all rows) and mean_no_nan (mean over the rows that have at least one bin bound). Both are plain averages of the per-bin values, not weighted by bin size.

Source code in ExcelMaster/Utility.py
def get_mean_risk(bivar_single_attr, value_range_col = ['min_indep', 'max_indep'], dep_col = "dep"):
    """get average risk

    Parameters
    ----------
    bivar_single_attr : pandas.DataFrame
        Bivariate table of one attribute with one row per bin.
    value_range_col : list of str, default ['min_indep', 'max_indep']
        Columns with the bounds of each bin. A row whose bounds are all missing is the missing-value bin and is left out of
        ``mean_no_nan``.
    dep_col : str, default "dep"
        Column with the bad rate of each bin.

    Returns
    -------
    pandas.DataFrame
        ``bivar_single_attr`` itself with two constant columns added in place: ``mean`` (mean of ``dep_col`` over all rows)
        and ``mean_no_nan`` (mean over the rows that have at least one bin bound). Both are plain averages of the per-bin
        values, not weighted by bin size.
    """
    mean_wo_nan = bivar_single_attr.dropna(how = "all", subset=value_range_col)[dep_col].mean()
    mean_w_na = bivar_single_attr[dep_col].mean()
    bivar_single_attr["mean"] = mean_w_na
    bivar_single_attr["mean_no_nan"] = mean_wo_nan
    return bivar_single_attr