data-visualiser-package 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ """
2
+ Data Visualiser Package
3
+
4
+ A utility package for visualizing and analyzing data with Matplotlib and Seaborn.
5
+ Creates plots and statistics tables for data exploration and presentation.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from .data_visualiser import DataVisualiser, categorise_variable_in_df, uncategorise
11
+
12
+ __all__ = ["DataVisualiser", "categorise_variable_in_df", "uncategorise"]
@@ -0,0 +1,643 @@
1
+ """
2
+ Data Visualiser Package
3
+
4
+ A utility package for visualizing and analyzing data with Matplotlib and Seaborn.
5
+ Creates plots and statistics tables for data exploration and presentation.
6
+
7
+ Quick import example:
8
+ ```python
9
+ from data_visualiser_package import DataVisualiser
10
+ dv = DataVisualiser()
11
+
12
+
13
+ ```
14
+ """
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional, Tuple, Union
18
+
19
+ import matplotlib.pyplot as plt
20
+ import numpy as np
21
+ import pandas as pd
22
+ import seaborn as sns
23
+
24
+ from .export_latex_tables import get_latex_table_bold_col_header, get_n_decimals_to_include
25
+
26
+ # Constants
27
+ RECORD_UNIT_NAME = "patient" # What a single record represents (e.g., patient in clinical data)
28
+ NAN_REPLACEMENT_STR = "NaN" # String to use when replacing NaN values for display
29
+ HEIGHT = 4 # Default plot height in inches
30
+
31
+
32
+ def uncategorise(col: pd.Series) -> pd.Series:
33
+ """
34
+ Convert categorical columns back to their original data types.
35
+
36
+ Args:
37
+ col: The pandas Series to convert
38
+
39
+ Returns:
40
+ The uncategorized pandas Series
41
+ """
42
+ if col.dtype.name == "category":
43
+ try:
44
+ return col.astype(col.cat.categories.dtype)
45
+ except:
46
+ # In case there is pd.NA (pandas >= 1.0), Int64 should be used instead of int64
47
+ return col.astype(col.cat.categories.dtype.name.title())
48
+ else:
49
+ return col
50
+
51
+
52
+ def categorise_variable_in_df(
53
+ var: str,
54
+ df: pd.DataFrame,
55
+ forced_order: Optional[List[str]] = None,
56
+ nan_replacement_str: str = NAN_REPLACEMENT_STR
57
+ ) -> None:
58
+ """
59
+ Categorize a variable in a DataFrame, handling NaN values and custom ordering.
60
+
61
+ Args:
62
+ var: Variable name to categorize
63
+ df: DataFrame containing the variable
64
+ forced_order: Optional custom ordering of categories
65
+ nan_replacement_str: String used to replace NaN values
66
+
67
+ Note:
68
+ This function modifies the DataFrame in-place.
69
+ """
70
+ if (df[var] == nan_replacement_str).any():
71
+ if forced_order is not None:
72
+ assert (set(df[var]) - {nan_replacement_str}).issubset(
73
+ forced_order
74
+ ), f"forced_order for var {var} is incomplete: {(set(df[var]) - set([nan_replacement_str])) - set(forced_order)}"
75
+ categories = forced_order + [nan_replacement_str]
76
+ else:
77
+ categories = sorted(set(df[var].unique()) - {nan_replacement_str}) + [
78
+ nan_replacement_str
79
+ ]
80
+ else:
81
+ if forced_order is not None:
82
+ assert (set(df[var]) - {np.nan}).issubset(
83
+ forced_order
84
+ ), f"forced_order for var {var} is incomplete: {(set(df[var]) - set([np.nan])) - set(forced_order)}"
85
+ categories = forced_order
86
+ else:
87
+ categories = sorted(set(df[var]) - {np.nan})
88
+
89
+ df[var] = pd.Categorical(df[var], categories=categories)
90
+
91
+
92
+ class DataVisualiser:
93
+ """
94
+ A class for creating and saving data visualizations and statistics tables.
95
+
96
+ This class provides methods to generate common statistical visualizations and tables,
97
+ including count plots, distribution plots, and their stratified versions.
98
+
99
+ Attributes:
100
+ record_unit_name: Name of what a single record represents (e.g., "patient")
101
+ count_colname: Column name used for count statistics
102
+ figures_dirpath: Directory path for saving figures
103
+ tables_dirpath: Directory path for saving tables
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ record_unit_name: str = RECORD_UNIT_NAME,
109
+ figures_dirpath: Optional[str] = None,
110
+ tables_dirpath: Optional[str] = None,
111
+ create_dirs: bool = False,
112
+ ):
113
+ """
114
+ Initialize DataVisualiser with paths and record unit information.
115
+
116
+ Args:
117
+ record_unit_name: Name of what a single record represents (e.g., "patient")
118
+ figures_dirpath: Directory path for saving figures
119
+ tables_dirpath: Directory path for saving tables
120
+ create_dirs: Whether to create the figures and tables directories if they don't exist
121
+ """
122
+ self.record_unit_name = record_unit_name
123
+ self.count_colname = f"# {record_unit_name}s"
124
+ self.figures_dirpath = figures_dirpath
125
+ self.tables_dirpath = tables_dirpath
126
+
127
+ if create_dirs and figures_dirpath and tables_dirpath:
128
+ Path(figures_dirpath).mkdir(parents=True, exist_ok=True)
129
+ Path(tables_dirpath).mkdir(parents=True, exist_ok=True)
130
+
131
+ def get_count_plot(
132
+ self,
133
+ var: str,
134
+ df: pd.DataFrame,
135
+ show_nan: bool = True,
136
+ save_fig: bool = False,
137
+ plot_kwargs: Dict = {},
138
+ xlabels_rotation: Optional[int] = None,
139
+ forced_order: Optional[List[str]] = None,
140
+ ) -> Tuple[plt.Figure, plt.Axes]:
141
+ """
142
+ Create a count plot for a categorical variable.
143
+
144
+ Args:
145
+ var: Variable to plot
146
+ df: DataFrame containing the data
147
+ show_nan: Whether to show NaN values as a separate category
148
+ save_fig: Whether to save the figure to disk
149
+ plot_kwargs: Additional keyword arguments to pass to seaborn's countplot
150
+ xlabels_rotation: Rotation angle for x-axis labels
151
+ forced_order: Optional custom ordering of categories
152
+
153
+ Returns:
154
+ Tuple of (figure, axes)
155
+ """
156
+ # Preprocess df
157
+ df = df[var].astype(str).to_frame().copy()
158
+ if show_nan:
159
+ df = df[var].fillna(NAN_REPLACEMENT_STR).to_frame().copy()
160
+ # Order variables alphabetically
161
+ categorise_variable_in_df(var, df, forced_order=forced_order)
162
+
163
+ fig, ax = plt.subplots()
164
+ sns.countplot(y=var, data=df, **plot_kwargs)
165
+ ax.set_title(f"Number of {self.record_unit_name}s separated by {var}")
166
+
167
+ if xlabels_rotation:
168
+ plt.xticks(rotation=xlabels_rotation)
169
+
170
+ if save_fig and self.figures_dirpath:
171
+ plt.savefig(
172
+ os.path.join(self.figures_dirpath, f"count_plot_{var}.png"),
173
+ bbox_inches="tight",
174
+ )
175
+ plt.close()
176
+
177
+ return fig, ax
178
+
179
+ def get_count_stratified_plot(
180
+ self,
181
+ var: str,
182
+ df: pd.DataFrame,
183
+ col: str = "sexe_desc",
184
+ show_nan: bool = True,
185
+ show_nan_col: bool = True,
186
+ save_fig: bool = False,
187
+ col_wrap: int = 3,
188
+ plot_kwargs: Dict = {},
189
+ xlabels_rotation: Optional[int] = None,
190
+ forced_order: Optional[List[str]] = None,
191
+ forced_order_col: Optional[List[str]] = None,
192
+ ) -> sns.FacetGrid:
193
+ """
194
+ Create stratified count plots for a categorical variable.
195
+
196
+ Args:
197
+ var: Variable to plot
198
+ df: DataFrame containing the data
199
+ col: Column to stratify by
200
+ show_nan: Whether to show NaN values in var as a separate category
201
+ show_nan_col: Whether to show NaN values in col as a separate category
202
+ save_fig: Whether to save the figure to disk
203
+ col_wrap: Number of facets per row
204
+ plot_kwargs: Additional keyword arguments to pass to seaborn's catplot
205
+ xlabels_rotation: Rotation angle for x-axis labels
206
+ forced_order: Optional custom ordering of categories for var
207
+ forced_order_col: Optional custom ordering of categories for col
208
+
209
+ Returns:
210
+ Seaborn FacetGrid object
211
+ """
212
+ # Preprocess df
213
+ df = df[[var, col]].astype(str).copy()
214
+ if show_nan:
215
+ df[var] = df[var].fillna(NAN_REPLACEMENT_STR)
216
+ if show_nan_col:
217
+ df[col] = df[col].fillna(NAN_REPLACEMENT_STR)
218
+ # Order variables alphabetically
219
+ categorise_variable_in_df(var, df, forced_order=forced_order)
220
+ categorise_variable_in_df(col, df, forced_order=forced_order_col)
221
+
222
+ # Use height from plot_kwargs if specified, otherwise use HEIGHT
223
+ if plot_kwargs.get("height") is None:
224
+ plot_kwargs["height"] = HEIGHT
225
+
226
+ fg = sns.catplot(
227
+ y=var, col=col, col_wrap=col_wrap, kind="count", data=df, **plot_kwargs
228
+ )
229
+ fg.fig.suptitle(
230
+ f"Number of {self.record_unit_name}s separated by {var} in each {col}",
231
+ y=1.02,
232
+ )
233
+
234
+ if xlabels_rotation:
235
+ fg.tick_params(axis="x", rotation=xlabels_rotation)
236
+
237
+ if save_fig and self.figures_dirpath:
238
+ plt.savefig(
239
+ os.path.join(
240
+ self.figures_dirpath, f"count_plot_{var}_for_each_{col}.png"
241
+ ),
242
+ bbox_inches="tight",
243
+ )
244
+ plt.close()
245
+
246
+ return fg
247
+
248
+ def get_count_stats_df(
249
+ self,
250
+ var: str,
251
+ df: pd.DataFrame,
252
+ show_nan: bool = True,
253
+ save_table: bool = False,
254
+ percentage: bool = True,
255
+ add_total: bool = False,
256
+ round_n_digits: int = 1,
257
+ forced_order: Optional[List[str]] = None,
258
+ ) -> pd.DataFrame:
259
+ """
260
+ Create a DataFrame with count statistics for a categorical variable.
261
+
262
+ Args:
263
+ var: Variable to analyze
264
+ df: DataFrame containing the data
265
+ show_nan: Whether to include NaN values as a separate category
266
+ save_table: Whether to save the statistics as a LaTeX table
267
+ percentage: Whether to include percentage column
268
+ add_total: Whether to add a "Total" row
269
+ round_n_digits: Number of decimal places to round to
270
+ forced_order: Optional custom ordering of categories
271
+
272
+ Returns:
273
+ DataFrame with count statistics
274
+ """
275
+ # Preprocess df
276
+ df = df[var].astype(str).to_frame().copy()
277
+ if show_nan:
278
+ df = df[var].fillna(NAN_REPLACEMENT_STR).to_frame().copy()
279
+ # Order variables alphabetically
280
+ categorise_variable_in_df(var, df, forced_order=forced_order)
281
+
282
+ # Compute stats df
283
+ stats_df = df.groupby(var).size().rename(self.count_colname).to_frame()
284
+
285
+ if percentage:
286
+ stats_df = pd.concat(
287
+ [
288
+ stats_df,
289
+ (100 * stats_df / stats_df.sum()).rename(
290
+ columns={self.count_colname: "%"}
291
+ ),
292
+ ],
293
+ axis=1,
294
+ )
295
+
296
+ if add_total:
297
+ stats_df = pd.concat(
298
+ [
299
+ stats_df,
300
+ stats_df.sum(axis=0).rename("Total").to_frame().transpose(),
301
+ ],
302
+ axis=0,
303
+ )
304
+ # Convert count_colname back to int type
305
+ stats_df[self.count_colname] = stats_df[self.count_colname].astype(int)
306
+
307
+ if save_table and self.tables_dirpath:
308
+ filepath = os.path.join(self.tables_dirpath, f"count_stats_table_{var}.tex")
309
+ with open(filepath, "w") as f:
310
+ f.write(stats_df.round(round_n_digits).to_latex())
311
+
312
+ return stats_df
313
+
314
+ def get_count_stratified_stats_df(
315
+ self,
316
+ var: str,
317
+ df: pd.DataFrame,
318
+ col: str = "sexe_desc",
319
+ show_nan: bool = True,
320
+ show_nan_col: bool = True,
321
+ save_table: bool = False,
322
+ percentage: bool = True,
323
+ round_n_digits: int = 1,
324
+ forced_order: Optional[List[str]] = None,
325
+ forced_order_col: Optional[List[str]] = None,
326
+ ) -> pd.DataFrame:
327
+ """
328
+ Create a DataFrame with stratified count statistics for a categorical variable.
329
+
330
+ Args:
331
+ var: Variable to analyze
332
+ df: DataFrame containing the data
333
+ col: Column to stratify by
334
+ show_nan: Whether to include NaN values in var as a separate category
335
+ show_nan_col: Whether to include NaN values in col as a separate category
336
+ save_table: Whether to save the statistics as a LaTeX table
337
+ percentage: Whether to include percentage column
338
+ round_n_digits: Number of decimal places to round to
339
+ forced_order: Optional custom ordering of categories for var
340
+ forced_order_col: Optional custom ordering of categories for col
341
+
342
+ Returns:
343
+ DataFrame with stratified count statistics
344
+ """
345
+ # Preprocess df
346
+ df = df[[var, col]].astype(str).copy()
347
+ if show_nan:
348
+ df[var] = df[var].fillna(NAN_REPLACEMENT_STR)
349
+ if show_nan_col:
350
+ df[col] = df[col].fillna(NAN_REPLACEMENT_STR)
351
+ # Order variables alphabetically
352
+ categorise_variable_in_df(var, df, forced_order=forced_order)
353
+ categorise_variable_in_df(col, df, forced_order=forced_order_col)
354
+
355
+ # Compute stats df
356
+ stratified_stats_df = pd.DataFrame(
357
+ df.groupby([col, var]).size(), columns=[self.count_colname]
358
+ )
359
+
360
+ if percentage:
361
+ stratified_stats_df = pd.concat(
362
+ [
363
+ stratified_stats_df,
364
+ (
365
+ 100
366
+ * stratified_stats_df
367
+ / stratified_stats_df.groupby(level=0).sum()
368
+ ).rename(columns={self.count_colname: "%"}),
369
+ ],
370
+ axis=1,
371
+ )
372
+
373
+ if save_table and self.tables_dirpath:
374
+ filepath = os.path.join(
375
+ self.tables_dirpath,
376
+ f"count_stats_table_{var}_for_each_{col}.tex",
377
+ )
378
+ with open(filepath, "w") as f:
379
+ f.write(stratified_stats_df.round(round_n_digits).to_latex())
380
+
381
+ return stratified_stats_df
382
+
383
+ def get_dist_plot(
384
+ self,
385
+ var: str,
386
+ df: pd.DataFrame,
387
+ save_fig: bool = False,
388
+ plot_kwargs: Dict = {"kde": True},
389
+ xlabels_rotation: Optional[int] = None
390
+ ) -> Tuple[plt.Figure, plt.Axes]:
391
+ """
392
+ Create a distribution plot for a numerical variable.
393
+
394
+ Args:
395
+ var: Variable to plot
396
+ df: DataFrame containing the data
397
+ save_fig: Whether to save the figure to disk
398
+ plot_kwargs: Additional keyword arguments to pass to seaborn's histplot
399
+ xlabels_rotation: Rotation angle for x-axis labels
400
+
401
+ Returns:
402
+ Tuple of (figure, axes)
403
+ """
404
+ fig, ax = plt.subplots()
405
+ sns.histplot(data=df, x=var, ax=ax, **plot_kwargs)
406
+ ax.set_title(f"Distribution of {var}")
407
+
408
+ if xlabels_rotation:
409
+ plt.xticks(rotation=xlabels_rotation)
410
+
411
+ if save_fig and self.figures_dirpath:
412
+ plt.savefig(
413
+ os.path.join(self.figures_dirpath, f"dist_plot_{var}.png"),
414
+ bbox_inches="tight",
415
+ )
416
+ plt.close()
417
+
418
+ return fig, ax
419
+
420
+ def get_dist_stratified_plot(
421
+ self,
422
+ var: str,
423
+ df: pd.DataFrame,
424
+ col: str = "sexe_desc",
425
+ show_nan_col: bool = True,
426
+ save_fig: bool = False,
427
+ col_wrap: int = 3,
428
+ plot_kwargs: Dict = {"kde": True},
429
+ xlabels_rotation: Optional[int] = None,
430
+ forced_order_col: Optional[List[str]] = None,
431
+ ) -> sns.FacetGrid:
432
+ """
433
+ Create stratified distribution plots for a numerical variable.
434
+
435
+ Args:
436
+ var: Variable to plot
437
+ df: DataFrame containing the data
438
+ col: Column to stratify by
439
+ show_nan_col: Whether to show NaN values in col as a separate category
440
+ save_fig: Whether to save the figure to disk
441
+ col_wrap: Number of facets per row
442
+ plot_kwargs: Additional keyword arguments to pass to seaborn's displot
443
+ xlabels_rotation: Rotation angle for x-axis labels
444
+ forced_order_col: Optional custom ordering of categories for col
445
+
446
+ Returns:
447
+ Seaborn FacetGrid object
448
+ """
449
+ # Preprocess df
450
+ df = df[[var, col]].copy()
451
+ df[col] = df[col].astype(str)
452
+ if show_nan_col:
453
+ df[col] = df[col].fillna(NAN_REPLACEMENT_STR)
454
+ # Order variables alphabetically
455
+ categorise_variable_in_df(col, df, forced_order=forced_order_col)
456
+
457
+ # Use height from plot_kwargs if specified, otherwise use HEIGHT
458
+ if plot_kwargs.get("height") is None:
459
+ plot_kwargs["height"] = HEIGHT
460
+
461
+ fg = sns.displot(data=df, x=var, col=col, col_wrap=col_wrap, **plot_kwargs)
462
+ fg.fig.suptitle(f"Distribution of {var} for each {col}", y=1.02)
463
+
464
+ if xlabels_rotation:
465
+ fg.set_xticklabels(rotation=xlabels_rotation)
466
+
467
+ if save_fig and self.figures_dirpath:
468
+ plt.savefig(
469
+ os.path.join(
470
+ self.figures_dirpath, f"dist_plot_{var}_for_each_{col}.png"
471
+ ),
472
+ bbox_inches="tight",
473
+ )
474
+ plt.close()
475
+
476
+ return fg
477
+
478
+ def get_dist_hued_plot(
479
+ self,
480
+ var: str,
481
+ df: pd.DataFrame,
482
+ hue: str = "sexe_desc",
483
+ show_nan_col: bool = True,
484
+ save_fig: bool = False,
485
+ plot_kwargs: Dict = {"kde": True},
486
+ xlabels_rotation: Optional[int] = None,
487
+ forced_order_col: Optional[List[str]] = None,
488
+ ) -> sns.FacetGrid:
489
+ """
490
+ Create a distribution plot for a numerical variable with hue for categories.
491
+
492
+ Args:
493
+ var: Variable to plot
494
+ df: DataFrame containing the data
495
+ hue: Column to use for color encoding
496
+ show_nan_col: Whether to show NaN values in hue as a separate category
497
+ save_fig: Whether to save the figure to disk
498
+ plot_kwargs: Additional keyword arguments to pass to seaborn's displot
499
+ xlabels_rotation: Rotation angle for x-axis labels
500
+ forced_order_col: Optional custom ordering of categories for hue
501
+
502
+ Returns:
503
+ Seaborn FacetGrid object
504
+ """
505
+ # Preprocess df
506
+ df = df[[var, hue]].copy()
507
+ df[hue] = df[hue].astype(str)
508
+ if show_nan_col:
509
+ df[hue] = df[hue].fillna(NAN_REPLACEMENT_STR)
510
+ # Order variables alphabetically
511
+ categorise_variable_in_df(hue, df, forced_order=forced_order_col)
512
+
513
+ # Use height from plot_kwargs if specified, otherwise use HEIGHT
514
+ if plot_kwargs.get("height") is None:
515
+ plot_kwargs["height"] = HEIGHT
516
+
517
+ fg = sns.displot(data=df, x=var, hue=hue, **plot_kwargs)
518
+ fg.fig.suptitle(f"Distribution of {var} for each {hue}", y=1.02)
519
+
520
+ if xlabels_rotation:
521
+ fg.set_xticklabels(rotation=xlabels_rotation)
522
+
523
+ if save_fig and self.figures_dirpath:
524
+ plt.savefig(
525
+ os.path.join(
526
+ self.figures_dirpath, f"dist_plot_{var}_for_hue_{hue}.png"
527
+ ),
528
+ bbox_inches="tight",
529
+ )
530
+ plt.close()
531
+
532
+ return fg
533
+
534
+ def get_dist_stats_df(
535
+ self,
536
+ var: str,
537
+ df: pd.DataFrame,
538
+ save_table: bool = False
539
+ ) -> pd.DataFrame:
540
+ """
541
+ Create a DataFrame with distribution statistics for a numerical variable.
542
+
543
+ Args:
544
+ var: Variable to analyze
545
+ df: DataFrame containing the data
546
+ save_table: Whether to save the statistics as a LaTeX table
547
+
548
+ Returns:
549
+ DataFrame with distribution statistics
550
+ """
551
+ stats_df = pd.DataFrame(df[var].describe()).transpose()
552
+
553
+ # Convert 'count' column into 'int' type
554
+ stats_df["count"] = stats_df["count"].astype(int)
555
+
556
+ # Add number and percentage of NaNs
557
+ stats_df.insert(1, "nan_perc", value=100 * df[var].isna().mean())
558
+ stats_df.insert(1, "n_nan", value=df[var].isna().sum())
559
+
560
+ if save_table and self.tables_dirpath:
561
+ # Get number of decimals to include
562
+ std = stats_df.squeeze()["std"]
563
+ round_n_digits = get_n_decimals_to_include(std)
564
+
565
+ filepath = os.path.join(self.tables_dirpath, f"dist_stats_table_{var}.tex")
566
+ with open(filepath, "w") as f:
567
+ f.write(
568
+ get_latex_table_bold_col_header(
569
+ stats_df, round_n_digits=round_n_digits
570
+ )
571
+ )
572
+
573
+ return stats_df
574
+
575
+ def get_dist_stratified_stats_df(
576
+ self,
577
+ var: str,
578
+ df: pd.DataFrame,
579
+ col: str = "sexe_desc",
580
+ show_nan_col: bool = True,
581
+ save_table: bool = False,
582
+ forced_order_col: Optional[List[str]] = None,
583
+ ) -> pd.DataFrame:
584
+ """
585
+ Create a DataFrame with stratified distribution statistics for a numerical variable.
586
+
587
+ Args:
588
+ var: Variable to analyze
589
+ df: DataFrame containing the data
590
+ col: Column to stratify by
591
+ show_nan_col: Whether to show NaN values in col as a separate category
592
+ save_table: Whether to save the statistics as a LaTeX table
593
+ forced_order_col: Optional custom ordering of categories for col
594
+
595
+ Returns:
596
+ DataFrame with stratified distribution statistics
597
+ """
598
+ # Preprocess df
599
+ df = df[[var, col]].copy()
600
+ df[col] = df[col].astype(str)
601
+ if show_nan_col:
602
+ df[col] = df[col].fillna(NAN_REPLACEMENT_STR)
603
+ # Order variables alphabetically
604
+ categorise_variable_in_df(col, df, forced_order=forced_order_col)
605
+
606
+ stratified_stats_df = pd.DataFrame(df.groupby(col)[var].describe())
607
+
608
+ # Convert 'count' column into 'int' type
609
+ stratified_stats_df["count"] = stratified_stats_df["count"].astype(int)
610
+
611
+ # Add number and percentage of NaNs
612
+ stratified_stats_df.insert(
613
+ 1,
614
+ "nan_perc",
615
+ value=100 * df.groupby(col)[var].apply(lambda x: x.isna().mean()),
616
+ )
617
+ stratified_stats_df.insert(
618
+ 1, "n_nan", value=df.groupby(col)[var].apply(lambda x: x.isna().sum())
619
+ )
620
+
621
+ if save_table and self.tables_dirpath:
622
+ # Get number of decimals to include
623
+ std = stratified_stats_df["std"].min()
624
+ round_n_digits = get_n_decimals_to_include(std)
625
+
626
+ filepath = os.path.join(
627
+ self.tables_dirpath,
628
+ f"dist_stats_table_{var}_for_each_{col}.tex",
629
+ )
630
+ with open(filepath, "w") as f:
631
+ f.write(
632
+ get_latex_table_bold_col_header(
633
+ stratified_stats_df, round_n_digits=round_n_digits
634
+ )
635
+ )
636
+
637
+ return stratified_stats_df
638
+
639
+
640
+ if __name__ == '__main__':
641
+ print("DataVisualiser: A tool for data visualization and analysis")
642
+ print("Import this module to use the functionality")
643
+ print("Example: from data_visualiser_package import DataVisualiser")
@@ -0,0 +1,102 @@
1
+ import os
2
+
3
+ import numpy as np
4
+
5
+
6
+ def remove_toprule(s):
7
+ return s.replace("\\toprule\n", "")
8
+
9
+
10
+ def remove_bottomrule(s):
11
+ return s.replace("\\bottomrule\n", "")
12
+
13
+
14
+ def replace_midrule_with_hline(s):
15
+ return s.replace("\\midrule", "\\hline")
16
+
17
+
18
+ def replace_bottomrule_with_hline(s):
19
+ return s.replace("\\bottomrule", "\\hline")
20
+
21
+
22
+ def change_header(s, header_list):
23
+ lines = s.split("\n")
24
+ header_line = lines[2]
25
+ new_header_line = " & ".join(header_list) + " \\\\"
26
+ new_lines = lines[:2] + [new_header_line] + lines[3:]
27
+ new_s = "\n".join(new_lines)
28
+
29
+ return new_s
30
+
31
+
32
+ def change_values_title(s, new_values_title):
33
+ lines = s.split("\n")
34
+ values_line = lines[-4]
35
+ values_list = values_line.split("&")
36
+
37
+ new_values_list = [new_values_title + " "] + values_list[1:]
38
+ new_values_line = "&".join(new_values_list)
39
+ new_lines = lines[:4] + [new_values_line] + lines[5:]
40
+ s = "\n".join(new_lines)
41
+
42
+ return s
43
+
44
+
45
+ def add_hline(s, line_number_from_end=3):
46
+ """Add hline at line_number_from_end.
47
+
48
+ The default '3' corresponds to
49
+ adding a line before the last line of the table.
50
+ """
51
+ lines = s.split("\n")
52
+ new_lines = (
53
+ lines[:-line_number_from_end] + ["\\hline"] + lines[-line_number_from_end:]
54
+ )
55
+
56
+ return "\n".join(new_lines)
57
+
58
+
59
+ def format_table(s, header_list=None, new_values_title=None):
60
+ if header_list is not None:
61
+ s = change_header(s, header_list)
62
+ if new_values_title is not None:
63
+ s = change_values_title(s, new_values_title)
64
+ s = remove_toprule(s)
65
+ s = remove_bottomrule(s)
66
+ s = replace_midrule_with_hline(s)
67
+ # s = replace_bottomrule_with_hline(s)
68
+ return s
69
+
70
+
71
+ def write_table_to_file(s, variable, dirpath):
72
+ with open(os.path.join(dirpath, variable + "_table.tex"), "w") as f:
73
+ f.write(s)
74
+
75
+
76
+ def boldify_line(line, words_to_boldify):
77
+ for word in words_to_boldify:
78
+ line = line.replace(word, "\\textbf{" + f"{word}" + "}")
79
+ return line
80
+
81
+
82
+ def get_latex_table_bold_col_header(df2, round_n_digits=1):
83
+ table_lines = df2.round(round_n_digits).to_latex().split("\\\\")
84
+
85
+ col_headers = list(df2.columns.str.replace("%", "\\%").str.replace("_", "\\_"))
86
+ new_line0 = boldify_line(table_lines[0], col_headers)
87
+
88
+ boldified_table = "\\\\".join([new_line0] + table_lines[1:])
89
+ return boldified_table
90
+
91
+
92
+ def get_n_decimals_to_include(std, min_n_decimals=1):
93
+ # If std == 0, return min_n_decimals, otherwise we get an error when applying the log
94
+ if std == 0:
95
+ return min_n_decimals
96
+
97
+ n_decimals_to_include = -(np.log10(std).round() - 1)
98
+
99
+ if n_decimals_to_include < 1:
100
+ return min_n_decimals
101
+ else:
102
+ return int(n_decimals_to_include)
File without changes
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-visualiser-package
3
+ Version: 0.1.0
4
+ Summary: A utility package for data visualization and statistical analysis with Matplotlib and Seaborn
5
+ Project-URL: Homepage, https://github.com/jonathan-doenz/data-visualiser-package
6
+ Project-URL: Bug Tracker, https://github.com/jonathan-doenz/data-visualiser-package/issues
7
+ Author-email: jonathan-doenz <jonathan.doenz@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Requires-Python: >=3.7
22
+ Requires-Dist: matplotlib>=3.3.0
23
+ Requires-Dist: numpy>=1.19.0
24
+ Requires-Dist: pandas>=1.0.0
25
+ Requires-Dist: seaborn>=0.11.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Data Visualiser Package
29
+
30
+ A Python utility package for data visualization and statistical analysis with Matplotlib and Seaborn. This package provides a simple interface to create common visualizations and statistical tables for exploratory data analysis and reporting.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install data-visualiser-package
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - Create count plots and distribution plots
41
+ - Generate stratified visualizations by categorical variables
42
+ - Compute and display statistical tables for numerical and categorical variables
43
+ - Save plots and tables to disk (including LaTeX export)
44
+ - Handling of missing values
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ import pandas as pd
50
+ from data_visualiser_package import DataVisualiser
51
+
52
+ # Create a DataVisualiser instance
53
+ dv = DataVisualiser(
54
+ record_unit_name="patient", # What a single record represents
55
+ figures_dirpath="./figures", # Where to save figures
56
+ tables_dirpath="./tables", # Where to save tables
57
+ create_dirs=True # Create directories if they don't exist
58
+ )
59
+
60
+ # Load your data
61
+ df = pd.read_csv("your_data.csv")
62
+
63
+ # Create a count plot
64
+ fig, ax = dv.get_count_plot(
65
+ var="diagnosis", # Categorical variable to plot
66
+ df=df, # DataFrame containing the data
67
+ show_nan=True, # Show NaN values as a separate category
68
+ save_fig=True # Save the figure to disk
69
+ )
70
+
71
+ # Create a distribution plot
72
+ fig, ax = dv.get_dist_plot(
73
+ var="age", # Numerical variable to plot
74
+ df=df, # DataFrame containing the data
75
+ save_fig=True # Save the figure to disk
76
+ )
77
+
78
+ # Generate statistics tables
79
+ stats_df = dv.get_count_stats_df(
80
+ var="diagnosis", # Categorical variable to analyze
81
+ df=df, # DataFrame containing the data
82
+ save_table=True # Save the table to disk as LaTeX
83
+ )
84
+
85
+ # Create stratified visualizations
86
+ fg = dv.get_dist_stratified_plot(
87
+ var="age", # Numerical variable to plot
88
+ df=df, # DataFrame containing the data
89
+ col="gender", # Categorical variable to stratify by
90
+ save_fig=True # Save the figure to disk
91
+ )
92
+ ```
93
+
94
+ ## Example
95
+
96
+ Here's a complete example of how to use the DataVisualiser class:
97
+
98
+ ```python
99
+ import pandas as pd
100
+ import numpy as np
101
+ from data_visualiser_package import DataVisualiser
102
+
103
+ # Create a sample dataset
104
+ np.random.seed(42)
105
+ n = 1000
106
+
107
+ # Generate sample data
108
+ data = {
109
+ 'age': np.random.normal(50, 15, n),
110
+ 'gender': np.random.choice(['Male', 'Female'], n),
111
+ 'diagnosis': np.random.choice(['Healthy', 'Condition A', 'Condition B', None], n, p=[0.6, 0.2, 0.15, 0.05]),
112
+ 'heart_rate': np.random.normal(80, 10, n),
113
+ 'blood_pressure': np.random.normal(120, 15, n)
114
+ }
115
+
116
+ # Create a DataFrame
117
+ df = pd.DataFrame(data)
118
+
119
+ # Initialize the DataVisualiser
120
+ dv = DataVisualiser(
121
+ record_unit_name="patient",
122
+ figures_dirpath="./output/figures",
123
+ tables_dirpath="./output/tables",
124
+ create_dirs=True
125
+ )
126
+
127
+ # Create plots
128
+ dv.get_count_plot('gender', df, save_fig=True)
129
+ dv.get_count_plot('diagnosis', df, save_fig=True)
130
+
131
+ # Create stratified count plots
132
+ dv.get_count_stratified_plot('diagnosis', df, col='gender', save_fig=True)
133
+
134
+ # Distribution plots
135
+ dv.get_dist_plot('age', df, save_fig=True)
136
+ dv.get_dist_stratified_plot('age', df, col='gender', save_fig=True)
137
+ dv.get_dist_hued_plot('age', df, hue='gender', save_fig=True)
138
+
139
+ # Generate statistics tables
140
+ dv.get_count_stats_df('diagnosis', df, save_table=True)
141
+ dv.get_dist_stats_df('age', df, save_table=True)
142
+ dv.get_dist_stratified_stats_df('age', df, col='gender', save_table=True)
143
+
144
+ print("All visualizations and tables have been generated successfully!")
145
+ ```
146
+
147
+ ## API Reference
148
+
149
+ ### DataVisualiser Class
150
+
151
+ ```python
152
+ class DataVisualiser(
153
+ record_unit_name="patient",
154
+ figures_dirpath=None,
155
+ tables_dirpath=None,
156
+ create_dirs=False
157
+ )
158
+ ```
159
+
160
+ #### Count Visualizations
161
+
162
+ - `get_count_plot(var, df, show_nan=True, save_fig=False, plot_kwargs={}, xlabels_rotation=None, forced_order=None)`
163
+ - `get_count_stratified_plot(var, df, col="gender", show_nan=True, show_nan_col=True, save_fig=False, col_wrap=3, plot_kwargs={}, xlabels_rotation=None, forced_order=None, forced_order_col=None)`
164
+ - `get_count_stats_df(var, df, show_nan=True, save_table=False, percentage=True, add_total=False, round_n_digits=1, forced_order=None)`
165
+ - `get_count_stratified_stats_df(var, df, col="gender", show_nan=True, show_nan_col=True, save_table=False, percentage=True, round_n_digits=1, forced_order=None, forced_order_col=None)`
166
+
167
+ #### Distribution Visualizations
168
+
169
+ - `get_dist_plot(var, df, save_fig=False, plot_kwargs={"kde": True}, xlabels_rotation=None)`
170
+ - `get_dist_stratified_plot(var, df, col="gender", show_nan_col=True, save_fig=False, col_wrap=3, plot_kwargs={"kde": True}, xlabels_rotation=None, forced_order_col=None)`
171
+ - `get_dist_hued_plot(var, df, hue="gender", show_nan_col=True, save_fig=False, plot_kwargs={"kde": True}, xlabels_rotation=None, forced_order_col=None)`
172
+ - `get_dist_stats_df(var, df, save_table=False)`
173
+ - `get_dist_stratified_stats_df(var, df, col="gender", show_nan_col=True, save_table=False, forced_order_col=None)`
174
+
175
+ ## License
176
+
177
+ MIT
178
+
179
+ ## Contributing
180
+
181
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,8 @@
1
+ data_visualiser_package/__init__.py,sha256=T1CoedsuYqnuk2XDHUAOdza90JWoiJsQG4Cj3sB1UQc,374
2
+ data_visualiser_package/data_visualiser.py,sha256=3yBLInKSc0BPIIZvCA6h1ybtG5IO6Ezm7V4aH71RLRg,22981
3
+ data_visualiser_package/export_latex_tables.py,sha256=cN9H_cey6NFvc1S9LOQWmK8qu-w0YenFvmO85riW0pk,2715
4
+ data_visualiser_package/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ data_visualiser_package-0.1.0.dist-info/METADATA,sha256=YIqDWOmIqxr3OMxCiVqKf_hgXJlStYFolSf6Q8tj-gY,6459
6
+ data_visualiser_package-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ data_visualiser_package-0.1.0.dist-info/licenses/LICENSE,sha256=IU2SLcxqnhJpreQXofrrzughr5gqNgcOoxb7CF585AY,1071
8
+ data_visualiser_package-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jonathan Doenz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.