fg-data-profiling 4.19.0__py2.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.
- data_profiling/__init__.py +34 -0
- data_profiling/compare_reports.py +359 -0
- data_profiling/config.py +496 -0
- data_profiling/config_default.yaml +223 -0
- data_profiling/config_minimal.yaml +222 -0
- data_profiling/controller/__init__.py +1 -0
- data_profiling/controller/console.py +125 -0
- data_profiling/controller/pandas_decorator.py +21 -0
- data_profiling/expectations_report.py +117 -0
- data_profiling/model/__init__.py +4 -0
- data_profiling/model/alerts.py +780 -0
- data_profiling/model/correlations.py +163 -0
- data_profiling/model/dataframe.py +35 -0
- data_profiling/model/describe.py +210 -0
- data_profiling/model/description.py +108 -0
- data_profiling/model/duplicates.py +14 -0
- data_profiling/model/expectation_algorithms.py +112 -0
- data_profiling/model/handler.py +81 -0
- data_profiling/model/missing.py +146 -0
- data_profiling/model/pairwise.py +33 -0
- data_profiling/model/pandas/__init__.py +55 -0
- data_profiling/model/pandas/correlations_pandas.py +207 -0
- data_profiling/model/pandas/dataframe_pandas.py +26 -0
- data_profiling/model/pandas/describe_boolean_pandas.py +43 -0
- data_profiling/model/pandas/describe_categorical_pandas.py +274 -0
- data_profiling/model/pandas/describe_counts_pandas.py +63 -0
- data_profiling/model/pandas/describe_date_pandas.py +77 -0
- data_profiling/model/pandas/describe_file_pandas.py +56 -0
- data_profiling/model/pandas/describe_generic_pandas.py +36 -0
- data_profiling/model/pandas/describe_image_pandas.py +255 -0
- data_profiling/model/pandas/describe_numeric_pandas.py +175 -0
- data_profiling/model/pandas/describe_path_pandas.py +63 -0
- data_profiling/model/pandas/describe_supported_pandas.py +41 -0
- data_profiling/model/pandas/describe_text_pandas.py +62 -0
- data_profiling/model/pandas/describe_timeseries_pandas.py +222 -0
- data_profiling/model/pandas/describe_url_pandas.py +57 -0
- data_profiling/model/pandas/discretize_pandas.py +81 -0
- data_profiling/model/pandas/duplicates_pandas.py +56 -0
- data_profiling/model/pandas/imbalance_pandas.py +35 -0
- data_profiling/model/pandas/missing_pandas.py +42 -0
- data_profiling/model/pandas/sample_pandas.py +38 -0
- data_profiling/model/pandas/summary_pandas.py +101 -0
- data_profiling/model/pandas/table_pandas.py +56 -0
- data_profiling/model/pandas/timeseries_index_pandas.py +33 -0
- data_profiling/model/pandas/utils_pandas.py +27 -0
- data_profiling/model/sample.py +37 -0
- data_profiling/model/spark/__init__.py +48 -0
- data_profiling/model/spark/correlations_spark.py +152 -0
- data_profiling/model/spark/dataframe_spark.py +34 -0
- data_profiling/model/spark/describe_boolean_spark.py +27 -0
- data_profiling/model/spark/describe_categorical_spark.py +28 -0
- data_profiling/model/spark/describe_counts_spark.py +105 -0
- data_profiling/model/spark/describe_date_spark.py +51 -0
- data_profiling/model/spark/describe_generic_spark.py +30 -0
- data_profiling/model/spark/describe_numeric_spark.py +155 -0
- data_profiling/model/spark/describe_supported_spark.py +33 -0
- data_profiling/model/spark/describe_text_spark.py +25 -0
- data_profiling/model/spark/duplicates_spark.py +54 -0
- data_profiling/model/spark/missing_spark.py +96 -0
- data_profiling/model/spark/sample_spark.py +43 -0
- data_profiling/model/spark/summary_spark.py +95 -0
- data_profiling/model/spark/table_spark.py +58 -0
- data_profiling/model/spark/timeseries_index_spark.py +12 -0
- data_profiling/model/summarizer.py +207 -0
- data_profiling/model/summary.py +66 -0
- data_profiling/model/summary_algorithms.py +276 -0
- data_profiling/model/table.py +10 -0
- data_profiling/model/timeseries_index.py +16 -0
- data_profiling/model/typeset.py +365 -0
- data_profiling/model/typeset_relations.py +143 -0
- data_profiling/profile_report.py +573 -0
- data_profiling/report/__init__.py +4 -0
- data_profiling/report/formatters.py +346 -0
- data_profiling/report/presentation/__init__.py +1 -0
- data_profiling/report/presentation/core/__init__.py +39 -0
- data_profiling/report/presentation/core/alerts.py +18 -0
- data_profiling/report/presentation/core/collapse.py +24 -0
- data_profiling/report/presentation/core/container.py +50 -0
- data_profiling/report/presentation/core/correlation_table.py +21 -0
- data_profiling/report/presentation/core/dropdown.py +44 -0
- data_profiling/report/presentation/core/duplicate.py +16 -0
- data_profiling/report/presentation/core/frequency_table.py +14 -0
- data_profiling/report/presentation/core/frequency_table_small.py +16 -0
- data_profiling/report/presentation/core/html.py +14 -0
- data_profiling/report/presentation/core/image.py +34 -0
- data_profiling/report/presentation/core/item_renderer.py +17 -0
- data_profiling/report/presentation/core/renderable.py +42 -0
- data_profiling/report/presentation/core/root.py +35 -0
- data_profiling/report/presentation/core/sample.py +20 -0
- data_profiling/report/presentation/core/scores.py +32 -0
- data_profiling/report/presentation/core/table.py +26 -0
- data_profiling/report/presentation/core/toggle_button.py +14 -0
- data_profiling/report/presentation/core/variable.py +40 -0
- data_profiling/report/presentation/core/variable_info.py +36 -0
- data_profiling/report/presentation/flavours/__init__.py +9 -0
- data_profiling/report/presentation/flavours/flavour_html.py +64 -0
- data_profiling/report/presentation/flavours/flavour_widget.py +61 -0
- data_profiling/report/presentation/flavours/flavours.py +43 -0
- data_profiling/report/presentation/flavours/html/__init__.py +47 -0
- data_profiling/report/presentation/flavours/html/alerts.py +10 -0
- data_profiling/report/presentation/flavours/html/collapse.py +7 -0
- data_profiling/report/presentation/flavours/html/container.py +58 -0
- data_profiling/report/presentation/flavours/html/correlation_table.py +13 -0
- data_profiling/report/presentation/flavours/html/dropdown.py +7 -0
- data_profiling/report/presentation/flavours/html/duplicate.py +24 -0
- data_profiling/report/presentation/flavours/html/frequency_table.py +20 -0
- data_profiling/report/presentation/flavours/html/frequency_table_small.py +15 -0
- data_profiling/report/presentation/flavours/html/html.py +6 -0
- data_profiling/report/presentation/flavours/html/image.py +7 -0
- data_profiling/report/presentation/flavours/html/root.py +14 -0
- data_profiling/report/presentation/flavours/html/sample.py +12 -0
- data_profiling/report/presentation/flavours/html/scores.py +11 -0
- data_profiling/report/presentation/flavours/html/table.py +7 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_constant.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_constant_length.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_dirty_category.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_duplicates.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_empty.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_high_cardinality.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_high_correlation.html +4 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_imbalance.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_infinite.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_missing.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_near_duplicates.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_non_stationary.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_seasonal.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_skewed.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_truncated.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_type_date.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_uniform.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_unique.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_unsupported.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts/alert_zeros.html +1 -0
- data_profiling/report/presentation/flavours/html/templates/alerts.html +47 -0
- data_profiling/report/presentation/flavours/html/templates/collapse.html +11 -0
- data_profiling/report/presentation/flavours/html/templates/correlation_table.html +5 -0
- data_profiling/report/presentation/flavours/html/templates/diagram.html +11 -0
- data_profiling/report/presentation/flavours/html/templates/dropdown.html +16 -0
- data_profiling/report/presentation/flavours/html/templates/duplicate.html +5 -0
- data_profiling/report/presentation/flavours/html/templates/frequency_table.html +45 -0
- data_profiling/report/presentation/flavours/html/templates/frequency_table_small.html +34 -0
- data_profiling/report/presentation/flavours/html/templates/report.html +26 -0
- data_profiling/report/presentation/flavours/html/templates/sample.html +10 -0
- data_profiling/report/presentation/flavours/html/templates/scores.html +78 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/batch_grid.html +16 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/grid.html +18 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/list.html +7 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/named_list.html +8 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/overview_tabs.html +30 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/scores.html +3 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/sections.html +13 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/select.html +40 -0
- data_profiling/report/presentation/flavours/html/templates/sequence/tabs.html +30 -0
- data_profiling/report/presentation/flavours/html/templates/table.html +38 -0
- data_profiling/report/presentation/flavours/html/templates/toggle_button.html +18 -0
- data_profiling/report/presentation/flavours/html/templates/variable.html +7 -0
- data_profiling/report/presentation/flavours/html/templates/variable_info.html +49 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/bootstrap.bundle.min.js +7 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/bootstrap.min.css +6 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/cosmo.bootstrap.min.css +12 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/flatly.bootstrap.min.css +12 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/script.js +52 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/simplex.bootstrap.min.css +12 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/style.css +253 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/assets/united.bootstrap.min.css +12 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/footer.html +7 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/javascript.html +18 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/navigation.html +36 -0
- data_profiling/report/presentation/flavours/html/templates/wrapper/style.html +53 -0
- data_profiling/report/presentation/flavours/html/templates.py +76 -0
- data_profiling/report/presentation/flavours/html/toggle_button.py +7 -0
- data_profiling/report/presentation/flavours/html/variable.py +7 -0
- data_profiling/report/presentation/flavours/html/variable_info.py +7 -0
- data_profiling/report/presentation/flavours/widget/__init__.py +49 -0
- data_profiling/report/presentation/flavours/widget/alerts.py +45 -0
- data_profiling/report/presentation/flavours/widget/collapse.py +43 -0
- data_profiling/report/presentation/flavours/widget/container.py +121 -0
- data_profiling/report/presentation/flavours/widget/correlation_table.py +14 -0
- data_profiling/report/presentation/flavours/widget/dropdown.py +31 -0
- data_profiling/report/presentation/flavours/widget/duplicate.py +14 -0
- data_profiling/report/presentation/flavours/widget/frequency_table.py +57 -0
- data_profiling/report/presentation/flavours/widget/frequency_table_small.py +66 -0
- data_profiling/report/presentation/flavours/widget/html.py +11 -0
- data_profiling/report/presentation/flavours/widget/image.py +26 -0
- data_profiling/report/presentation/flavours/widget/notebook.py +81 -0
- data_profiling/report/presentation/flavours/widget/root.py +10 -0
- data_profiling/report/presentation/flavours/widget/sample.py +14 -0
- data_profiling/report/presentation/flavours/widget/table.py +30 -0
- data_profiling/report/presentation/flavours/widget/toggle_button.py +17 -0
- data_profiling/report/presentation/flavours/widget/variable.py +12 -0
- data_profiling/report/presentation/flavours/widget/variable_info.py +11 -0
- data_profiling/report/presentation/frequency_table_utils.py +141 -0
- data_profiling/report/structure/__init__.py +1 -0
- data_profiling/report/structure/correlations.py +123 -0
- data_profiling/report/structure/overview.py +376 -0
- data_profiling/report/structure/report.py +457 -0
- data_profiling/report/structure/variables/__init__.py +35 -0
- data_profiling/report/structure/variables/render_boolean.py +132 -0
- data_profiling/report/structure/variables/render_categorical.py +566 -0
- data_profiling/report/structure/variables/render_common.py +31 -0
- data_profiling/report/structure/variables/render_complex.py +102 -0
- data_profiling/report/structure/variables/render_count.py +172 -0
- data_profiling/report/structure/variables/render_date.py +143 -0
- data_profiling/report/structure/variables/render_file.py +70 -0
- data_profiling/report/structure/variables/render_generic.py +45 -0
- data_profiling/report/structure/variables/render_image.py +204 -0
- data_profiling/report/structure/variables/render_path.py +134 -0
- data_profiling/report/structure/variables/render_real.py +314 -0
- data_profiling/report/structure/variables/render_text.py +189 -0
- data_profiling/report/structure/variables/render_timeseries.py +371 -0
- data_profiling/report/structure/variables/render_url.py +132 -0
- data_profiling/report/utils.py +34 -0
- data_profiling/serialize_report.py +143 -0
- data_profiling/utils/__init__.py +1 -0
- data_profiling/utils/backend.py +9 -0
- data_profiling/utils/cache.py +59 -0
- data_profiling/utils/common.py +142 -0
- data_profiling/utils/compat.py +31 -0
- data_profiling/utils/dataframe.py +238 -0
- data_profiling/utils/logger.py +53 -0
- data_profiling/utils/notebook.py +8 -0
- data_profiling/utils/paths.py +45 -0
- data_profiling/utils/progress_bar.py +15 -0
- data_profiling/utils/styles.py +22 -0
- data_profiling/utils/versions.py +19 -0
- data_profiling/version.py +1 -0
- data_profiling/visualisation/__init__.py +1 -0
- data_profiling/visualisation/context.py +87 -0
- data_profiling/visualisation/missing.py +138 -0
- data_profiling/visualisation/plot.py +1158 -0
- data_profiling/visualisation/utils.py +113 -0
- fg_data_profiling-4.19.0.dist-info/METADATA +362 -0
- fg_data_profiling-4.19.0.dist-info/RECORD +238 -0
- fg_data_profiling-4.19.0.dist-info/WHEEL +6 -0
- fg_data_profiling-4.19.0.dist-info/entry_points.txt +3 -0
- fg_data_profiling-4.19.0.dist-info/licenses/LICENSE +21 -0
- fg_data_profiling-4.19.0.dist-info/top_level.txt +2 -0
- ydata_profiling/__init__.py +43 -0
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
"""Plot functions for the profiling report."""
|
|
2
|
+
import copy
|
|
3
|
+
from typing import Any, Callable, List, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import matplotlib
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import seaborn as sns
|
|
9
|
+
from matplotlib import pyplot as plt
|
|
10
|
+
from matplotlib.collections import PolyCollection
|
|
11
|
+
from matplotlib.colors import Colormap, LinearSegmentedColormap, ListedColormap, rgb2hex
|
|
12
|
+
from matplotlib.dates import AutoDateLocator, ConciseDateFormatter
|
|
13
|
+
from matplotlib.patches import Patch
|
|
14
|
+
from matplotlib.ticker import FuncFormatter, MaxNLocator
|
|
15
|
+
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
|
16
|
+
from typeguard import typechecked
|
|
17
|
+
from wordcloud import WordCloud
|
|
18
|
+
|
|
19
|
+
from data_profiling.config import Settings
|
|
20
|
+
from data_profiling.utils.common import convert_timestamp_to_datetime
|
|
21
|
+
from data_profiling.visualisation.context import manage_matplotlib_context
|
|
22
|
+
from data_profiling.visualisation.utils import plot_360_n0sc0pe
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_fn(tick_val: int, tick_pos: Any) -> str:
|
|
26
|
+
return convert_timestamp_to_datetime(tick_val).strftime("%Y-%m-%d %H:%M:%S")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _plot_word_cloud(
|
|
30
|
+
config: Settings,
|
|
31
|
+
series: Union[pd.Series, List[pd.Series]],
|
|
32
|
+
figsize: tuple = (6, 4),
|
|
33
|
+
) -> plt.Figure:
|
|
34
|
+
if not isinstance(series, list):
|
|
35
|
+
series = [series]
|
|
36
|
+
plot = plt.figure(figsize=figsize)
|
|
37
|
+
for i, series_data in enumerate(series):
|
|
38
|
+
word_dict = series_data.to_dict()
|
|
39
|
+
wordcloud = WordCloud(
|
|
40
|
+
font_path=config.plot.font_path,
|
|
41
|
+
background_color="white",
|
|
42
|
+
random_state=123,
|
|
43
|
+
width=300,
|
|
44
|
+
height=200,
|
|
45
|
+
scale=2,
|
|
46
|
+
).generate_from_frequencies(word_dict)
|
|
47
|
+
|
|
48
|
+
ax = plot.add_subplot(1, len(series), i + 1)
|
|
49
|
+
ax.imshow(wordcloud)
|
|
50
|
+
ax.axis("off")
|
|
51
|
+
|
|
52
|
+
return plot
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _plot_histogram(
|
|
56
|
+
config: Settings,
|
|
57
|
+
series: np.ndarray,
|
|
58
|
+
bins: Union[int, np.ndarray],
|
|
59
|
+
figsize: tuple = (6, 4),
|
|
60
|
+
date: bool = False,
|
|
61
|
+
hide_yaxis: bool = False,
|
|
62
|
+
) -> plt.Figure:
|
|
63
|
+
"""Plot a histogram from the data and return the AxesSubplot object.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
config: the Settings object
|
|
67
|
+
series: The data to plot
|
|
68
|
+
bins: number of bins (int for equal size, ndarray for variable size)
|
|
69
|
+
figsize: The size of the figure (width, height) in inches, default (6,4)
|
|
70
|
+
date: is the x-axis of date type
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The histogram plot.
|
|
74
|
+
"""
|
|
75
|
+
# we have precomputed the histograms...
|
|
76
|
+
if isinstance(bins, list):
|
|
77
|
+
n_labels = len(config.html.style._labels)
|
|
78
|
+
fig = plt.figure(figsize=figsize)
|
|
79
|
+
plot = fig.add_subplot(111)
|
|
80
|
+
|
|
81
|
+
for idx in reversed(list(range(n_labels))):
|
|
82
|
+
if len(bins):
|
|
83
|
+
diff = np.diff(bins[idx])
|
|
84
|
+
plot.bar(
|
|
85
|
+
bins[idx][:-1] + diff / 2, # type: ignore
|
|
86
|
+
series[idx],
|
|
87
|
+
diff,
|
|
88
|
+
facecolor=config.html.style.primary_colors[idx],
|
|
89
|
+
alpha=0.6,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if date:
|
|
93
|
+
plot.xaxis.set_major_formatter(FuncFormatter(format_fn))
|
|
94
|
+
|
|
95
|
+
if not config.plot.histogram.x_axis_labels:
|
|
96
|
+
plot.set_xticklabels([])
|
|
97
|
+
|
|
98
|
+
if hide_yaxis:
|
|
99
|
+
plot.yaxis.set_visible(False)
|
|
100
|
+
|
|
101
|
+
if not config.plot.histogram.x_axis_labels:
|
|
102
|
+
fig.xticklabels([])
|
|
103
|
+
|
|
104
|
+
if not hide_yaxis:
|
|
105
|
+
fig.supylabel("Frequency")
|
|
106
|
+
else:
|
|
107
|
+
fig = plt.figure(figsize=figsize)
|
|
108
|
+
plot = fig.add_subplot(111)
|
|
109
|
+
if not hide_yaxis:
|
|
110
|
+
plot.set_ylabel("Frequency")
|
|
111
|
+
else:
|
|
112
|
+
plot.axes.get_yaxis().set_visible(False)
|
|
113
|
+
|
|
114
|
+
diff = np.diff(bins)
|
|
115
|
+
plot.bar(
|
|
116
|
+
bins[:-1] + diff / 2, # type: ignore
|
|
117
|
+
series,
|
|
118
|
+
diff,
|
|
119
|
+
facecolor=config.html.style.primary_colors[0],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if date:
|
|
123
|
+
plot.xaxis.set_major_formatter(FuncFormatter(format_fn))
|
|
124
|
+
|
|
125
|
+
if not config.plot.histogram.x_axis_labels:
|
|
126
|
+
plot.set_xticklabels([])
|
|
127
|
+
|
|
128
|
+
return plot
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@manage_matplotlib_context()
|
|
132
|
+
def plot_word_cloud(config: Settings, word_counts: pd.Series) -> str:
|
|
133
|
+
_plot_word_cloud(config=config, series=word_counts)
|
|
134
|
+
return plot_360_n0sc0pe(config)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _is_valid_hist_data(series: np.ndarray, bins: Union[int, np.ndarray]) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Returns True if the series and bins contain enough usable numeric data
|
|
140
|
+
to produce a histogram without matplotlib errors.
|
|
141
|
+
"""
|
|
142
|
+
if series is None or bins is None:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
if len(series) == 0:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
series_arr = np.asarray(series, dtype=float)
|
|
150
|
+
except Exception:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
if not np.isfinite(series_arr).any():
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Handle bins type
|
|
157
|
+
if isinstance(bins, int):
|
|
158
|
+
if bins < 1:
|
|
159
|
+
return False
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
bins_arr = np.asarray(bins, dtype=float)
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
if len(bins_arr) < 2:
|
|
166
|
+
return False
|
|
167
|
+
if not np.isfinite(bins_arr).all():
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@manage_matplotlib_context()
|
|
174
|
+
def histogram(
|
|
175
|
+
config: Settings,
|
|
176
|
+
series: np.ndarray,
|
|
177
|
+
bins: Union[int, np.ndarray],
|
|
178
|
+
date: bool = False,
|
|
179
|
+
) -> str | None:
|
|
180
|
+
"""Plot an histogram of the data.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
config: Settings
|
|
184
|
+
series: The data to plot.
|
|
185
|
+
bins: number of bins (int for equal size, ndarray for variable size)
|
|
186
|
+
date: is histogram of date(time)?
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The resulting histogram encoded as a string.
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
if not _is_valid_hist_data(series, bins):
|
|
194
|
+
return None
|
|
195
|
+
plot = _plot_histogram(config, series, bins, date=date, figsize=(7, 3))
|
|
196
|
+
plot.xaxis.set_tick_params(rotation=90 if date else 45)
|
|
197
|
+
plot.figure.tight_layout()
|
|
198
|
+
return plot_360_n0sc0pe(config)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@manage_matplotlib_context()
|
|
202
|
+
def mini_histogram(
|
|
203
|
+
config: Settings,
|
|
204
|
+
series: np.ndarray,
|
|
205
|
+
bins: Union[int, np.ndarray],
|
|
206
|
+
date: bool = False,
|
|
207
|
+
) -> str | None:
|
|
208
|
+
"""Plot a small (mini) histogram of the data.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
config: Settings
|
|
212
|
+
series: The data to plot.
|
|
213
|
+
bins: number of bins (int for equal size, ndarray for variable size)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
The resulting mini histogram encoded as a string.
|
|
217
|
+
"""
|
|
218
|
+
if not _is_valid_hist_data(series, bins):
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
plot = _plot_histogram(
|
|
222
|
+
config, series, bins, figsize=(3, 2.25), date=date, hide_yaxis=True
|
|
223
|
+
)
|
|
224
|
+
plot.set_facecolor("w")
|
|
225
|
+
|
|
226
|
+
for tick in plot.xaxis.get_major_ticks():
|
|
227
|
+
tick.label1.set_fontsize(6 if date else 8)
|
|
228
|
+
plot.xaxis.set_tick_params(rotation=90 if date else 45)
|
|
229
|
+
plot.figure.tight_layout()
|
|
230
|
+
|
|
231
|
+
return plot_360_n0sc0pe(config)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_cmap_half(
|
|
235
|
+
cmap: Union[Colormap, LinearSegmentedColormap, ListedColormap]
|
|
236
|
+
) -> LinearSegmentedColormap:
|
|
237
|
+
"""Get the upper half of the color map
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
cmap: the color map
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
A new color map based on the upper half of another color map
|
|
244
|
+
|
|
245
|
+
References:
|
|
246
|
+
https://stackoverflow.com/a/24746399/470433
|
|
247
|
+
"""
|
|
248
|
+
# Evaluate an existing colormap from 0.5 (midpoint) to 1 (upper end)
|
|
249
|
+
colors = cmap(np.linspace(0.5, 1, cmap.N // 2))
|
|
250
|
+
|
|
251
|
+
# Create a new colormap from those colors
|
|
252
|
+
return LinearSegmentedColormap.from_list("cmap_half", colors)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_correlation_font_size(n_labels: int) -> Optional[int]:
|
|
256
|
+
"""Dynamic label font sizes in correlation plots
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
n_labels: the number of labels
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
A font size or None for the default font size
|
|
263
|
+
"""
|
|
264
|
+
if n_labels > 100:
|
|
265
|
+
font_size = 4
|
|
266
|
+
elif n_labels > 80:
|
|
267
|
+
font_size = 5
|
|
268
|
+
elif n_labels > 50:
|
|
269
|
+
font_size = 6
|
|
270
|
+
elif n_labels > 40:
|
|
271
|
+
font_size = 8
|
|
272
|
+
else:
|
|
273
|
+
return None
|
|
274
|
+
return font_size
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@manage_matplotlib_context()
|
|
278
|
+
def correlation_matrix(config: Settings, data: pd.DataFrame, vmin: int = -1) -> str:
|
|
279
|
+
"""Plot image of a matrix correlation.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
config: Settings
|
|
283
|
+
data: The matrix correlation to plot.
|
|
284
|
+
vmin: Minimum value of value range.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
The resulting correlation matrix encoded as a string.
|
|
288
|
+
"""
|
|
289
|
+
fig_cor, axes_cor = plt.subplots()
|
|
290
|
+
|
|
291
|
+
cmap = plt.get_cmap(config.plot.correlation.cmap)
|
|
292
|
+
if vmin == 0:
|
|
293
|
+
cmap = get_cmap_half(cmap)
|
|
294
|
+
cmap = copy.copy(cmap)
|
|
295
|
+
cmap.set_bad(config.plot.correlation.bad)
|
|
296
|
+
|
|
297
|
+
labels = data.columns
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
matrix = np.asarray(data, dtype=float)
|
|
301
|
+
except Exception:
|
|
302
|
+
# If conversion fails, create an all-NaN matrix of the appropriate shape
|
|
303
|
+
n = len(data)
|
|
304
|
+
matrix = np.full((n, n), np.nan, dtype=float)
|
|
305
|
+
|
|
306
|
+
matrix_image = axes_cor.imshow(
|
|
307
|
+
matrix, vmin=vmin, vmax=1, interpolation="nearest", cmap=cmap
|
|
308
|
+
)
|
|
309
|
+
plt.colorbar(matrix_image)
|
|
310
|
+
|
|
311
|
+
if data.isnull().values.any():
|
|
312
|
+
legend_elements = [Patch(facecolor=cmap(np.nan), label="invalid\ncoefficient")]
|
|
313
|
+
|
|
314
|
+
plt.legend(
|
|
315
|
+
handles=legend_elements,
|
|
316
|
+
loc="upper right",
|
|
317
|
+
handleheight=2.5,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
axes_cor.set_xticks(np.arange(0, data.shape[0], float(data.shape[0]) / len(labels)))
|
|
321
|
+
axes_cor.set_yticks(np.arange(0, data.shape[1], float(data.shape[1]) / len(labels)))
|
|
322
|
+
|
|
323
|
+
font_size = get_correlation_font_size(len(labels))
|
|
324
|
+
axes_cor.set_xticklabels(labels, rotation=90, fontsize=font_size)
|
|
325
|
+
axes_cor.set_yticklabels(labels, fontsize=font_size)
|
|
326
|
+
plt.subplots_adjust(bottom=0.2)
|
|
327
|
+
|
|
328
|
+
return plot_360_n0sc0pe(config)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@manage_matplotlib_context()
|
|
332
|
+
def scatter_complex(config: Settings, series: pd.Series) -> str:
|
|
333
|
+
"""Scatter plot (or hexbin plot) from a series of complex values
|
|
334
|
+
|
|
335
|
+
Examples:
|
|
336
|
+
>>> complex_series = pd.Series([complex(1, 3), complex(3, 1)])
|
|
337
|
+
>>> scatter_complex(complex_series)
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
config: Settings
|
|
341
|
+
series: the Series
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
A string containing (a reference to) the image
|
|
345
|
+
"""
|
|
346
|
+
plt.ylabel("Imaginary")
|
|
347
|
+
plt.xlabel("Real")
|
|
348
|
+
|
|
349
|
+
color = config.html.style.primary_colors[0]
|
|
350
|
+
|
|
351
|
+
if len(series) > config.plot.scatter_threshold:
|
|
352
|
+
cmap = sns.light_palette(color, as_cmap=True)
|
|
353
|
+
plt.hexbin(series.real, series.imag, cmap=cmap)
|
|
354
|
+
else:
|
|
355
|
+
plt.scatter(series.real, series.imag, color=color)
|
|
356
|
+
|
|
357
|
+
return plot_360_n0sc0pe(config)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@manage_matplotlib_context()
|
|
361
|
+
def scatter_series(
|
|
362
|
+
config: Settings, series: pd.Series, x_label: str = "Width", y_label: str = "Height"
|
|
363
|
+
) -> str:
|
|
364
|
+
"""Scatter plot (or hexbin plot) from one series of sequences with length 2
|
|
365
|
+
|
|
366
|
+
Examples:
|
|
367
|
+
>>> scatter_series(file_sizes, "Width", "Height")
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
config: report Settings object
|
|
371
|
+
series: the Series
|
|
372
|
+
x_label: the label on the x-axis
|
|
373
|
+
y_label: the label on the y-axis
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
A string containing (a reference to) the image
|
|
377
|
+
"""
|
|
378
|
+
plt.xlabel(x_label)
|
|
379
|
+
plt.ylabel(y_label)
|
|
380
|
+
|
|
381
|
+
color = config.html.style.primary_colors[0]
|
|
382
|
+
|
|
383
|
+
data = zip(*series.tolist())
|
|
384
|
+
if len(series) > config.plot.scatter_threshold:
|
|
385
|
+
cmap = sns.light_palette(color, as_cmap=True)
|
|
386
|
+
plt.hexbin(*data, cmap=cmap)
|
|
387
|
+
else:
|
|
388
|
+
plt.scatter(*data, color=color)
|
|
389
|
+
return plot_360_n0sc0pe(config)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@manage_matplotlib_context()
|
|
393
|
+
def scatter_pairwise(
|
|
394
|
+
config: Settings, series1: pd.Series, series2: pd.Series, x_label: str, y_label: str
|
|
395
|
+
) -> str:
|
|
396
|
+
"""Scatter plot (or hexbin plot) from two series
|
|
397
|
+
|
|
398
|
+
Examples:
|
|
399
|
+
>>> widths = pd.Series([800, 1024])
|
|
400
|
+
>>> heights = pd.Series([600, 768])
|
|
401
|
+
>>> scatter_series(widths, heights, "Width", "Height")
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
config: Settings
|
|
405
|
+
series1: the series corresponding to the x-axis
|
|
406
|
+
series2: the series corresponding to the y-axis
|
|
407
|
+
x_label: the label on the x-axis
|
|
408
|
+
y_label: the label on the y-axis
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
A string containing (a reference to) the image
|
|
412
|
+
"""
|
|
413
|
+
plt.xlabel(x_label)
|
|
414
|
+
plt.ylabel(y_label)
|
|
415
|
+
|
|
416
|
+
color = config.html.style.primary_colors[0]
|
|
417
|
+
|
|
418
|
+
indices = (series1.notna()) & (series2.notna())
|
|
419
|
+
if len(series1) > config.plot.scatter_threshold:
|
|
420
|
+
cmap = sns.light_palette(color, as_cmap=True)
|
|
421
|
+
plt.hexbin(series1[indices], series2[indices], gridsize=15, cmap=cmap)
|
|
422
|
+
else:
|
|
423
|
+
plt.scatter(series1[indices], series2[indices], color=color)
|
|
424
|
+
return plot_360_n0sc0pe(config)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _plot_stacked_barh(
|
|
428
|
+
data: pd.Series, colors: List, hide_legend: bool = False
|
|
429
|
+
) -> Tuple[plt.Axes, matplotlib.legend.Legend]:
|
|
430
|
+
"""Plot a stacked horizontal bar chart to show category frequency.
|
|
431
|
+
Works for boolean and categorical features.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
data (pd.Series): category frequencies with category names as index
|
|
435
|
+
colors (list): list of colors in a valid matplotlib format
|
|
436
|
+
hide_legend (bool): if true, the legend is omitted
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
ax: Stacked bar plot (matplotlib.axes)
|
|
440
|
+
legend: Legend handler (matplotlib)
|
|
441
|
+
"""
|
|
442
|
+
# Use the pd.Series indices as category names
|
|
443
|
+
labels = data.index.values.astype(str)
|
|
444
|
+
|
|
445
|
+
# Plot
|
|
446
|
+
_, ax = plt.subplots(figsize=(7, 2))
|
|
447
|
+
ax.axis("off")
|
|
448
|
+
|
|
449
|
+
ax.set_xlim(0, np.sum(data))
|
|
450
|
+
ax.set_ylim(0.4, 1.6)
|
|
451
|
+
|
|
452
|
+
starts = 0
|
|
453
|
+
for x, label, color in zip(data, labels, colors):
|
|
454
|
+
# Add a rectangle to the stacked barh chart
|
|
455
|
+
rects = ax.barh(y=1, width=x, height=1, left=starts, label=label, color=color)
|
|
456
|
+
|
|
457
|
+
# Label color depends on the darkness of the rectangle
|
|
458
|
+
r, g, b, _ = rects[0].get_facecolor()
|
|
459
|
+
text_color = "white" if r * g * b < 0.5 else "darkgrey"
|
|
460
|
+
|
|
461
|
+
# If the new bar is big enough write the label
|
|
462
|
+
pc_of_total = x / data.sum() * 100
|
|
463
|
+
# Requires matplotlib >= 3.4.0
|
|
464
|
+
if pc_of_total > 8 and hasattr(ax, "bar_label"):
|
|
465
|
+
display_txt = f"{pc_of_total:.1f}%\n({x})"
|
|
466
|
+
ax.bar_label(
|
|
467
|
+
rects,
|
|
468
|
+
labels=[display_txt],
|
|
469
|
+
label_type="center",
|
|
470
|
+
color=text_color,
|
|
471
|
+
fontsize="x-large",
|
|
472
|
+
fontweight="bold",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
starts += x
|
|
476
|
+
|
|
477
|
+
legend = None
|
|
478
|
+
if not hide_legend:
|
|
479
|
+
legend = ax.legend(
|
|
480
|
+
ncol=1, bbox_to_anchor=(0, 0), fontsize="xx-large", loc="upper left"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
return ax, legend
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _plot_pie_chart(
|
|
487
|
+
data: pd.Series, colors: List, hide_legend: bool = False
|
|
488
|
+
) -> Tuple[plt.Axes, matplotlib.legend.Legend]:
|
|
489
|
+
"""Plot a pie chart to show category frequency.
|
|
490
|
+
Works for boolean and categorical features.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
data (pd.Series): category frequencies with category names as index
|
|
494
|
+
colors (list): list of colors in a valid matplotlib format
|
|
495
|
+
hide_legend (bool): if true, the legend is omitted
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
ax: pie chart (matplotlib.axes)
|
|
499
|
+
legend: Legend handler (matplotlib)
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
def make_autopct(values: pd.Series) -> Callable:
|
|
503
|
+
def my_autopct(pct: float) -> str:
|
|
504
|
+
total = np.sum(values)
|
|
505
|
+
val = int(round(pct * total / 100.0))
|
|
506
|
+
return f"{pct:.1f}% ({val:d})"
|
|
507
|
+
|
|
508
|
+
return my_autopct
|
|
509
|
+
|
|
510
|
+
_, ax = plt.subplots(figsize=(4, 4))
|
|
511
|
+
wedges, _, _ = plt.pie(
|
|
512
|
+
data,
|
|
513
|
+
autopct=make_autopct(data),
|
|
514
|
+
textprops={"color": "w"},
|
|
515
|
+
colors=colors,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
legend = None
|
|
519
|
+
if not hide_legend:
|
|
520
|
+
legend = plt.legend(
|
|
521
|
+
wedges,
|
|
522
|
+
data.index.values,
|
|
523
|
+
fontsize="large",
|
|
524
|
+
bbox_to_anchor=(0, 0),
|
|
525
|
+
loc="upper left",
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
return ax, legend
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@manage_matplotlib_context()
|
|
532
|
+
def cat_frequency_plot(
|
|
533
|
+
config: Settings,
|
|
534
|
+
data: pd.Series,
|
|
535
|
+
) -> str:
|
|
536
|
+
"""Generate category frequency plot to show category frequency.
|
|
537
|
+
Works for boolean and categorical features.
|
|
538
|
+
|
|
539
|
+
Modify colors by setting 'config.plot.cat_freq.colors' to a
|
|
540
|
+
list of valid matplotib colors:
|
|
541
|
+
https://matplotlib.org/stable/tutorials/colors/colors.html
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
config (Settings): a profile report config
|
|
545
|
+
data (pd.Series): category frequencies with category names as index
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
str: encoded category frequency plot encoded
|
|
549
|
+
"""
|
|
550
|
+
# Get colors, if not defined, use matplotlib defaults
|
|
551
|
+
colors = config.plot.cat_freq.colors
|
|
552
|
+
if colors is None:
|
|
553
|
+
# Get matplotlib defaults
|
|
554
|
+
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
555
|
+
|
|
556
|
+
# If there are more categories than colors, loop through the colors again
|
|
557
|
+
if len(colors) < len(data):
|
|
558
|
+
multiplier = int(len(data) / len(colors)) + 1
|
|
559
|
+
colors = multiplier * colors # repeat colors as required
|
|
560
|
+
colors = colors[0 : len(data)] # select the exact number of colors required
|
|
561
|
+
|
|
562
|
+
# Create the plot
|
|
563
|
+
plot_type = config.plot.cat_freq.type
|
|
564
|
+
if plot_type == "bar":
|
|
565
|
+
if isinstance(data, list):
|
|
566
|
+
for v in data:
|
|
567
|
+
plot, legend = _plot_stacked_barh(
|
|
568
|
+
v, colors, hide_legend=config.vars.cat.redact
|
|
569
|
+
)
|
|
570
|
+
else:
|
|
571
|
+
plot, legend = _plot_stacked_barh(
|
|
572
|
+
data, colors, hide_legend=config.vars.cat.redact
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
elif plot_type == "pie":
|
|
576
|
+
plot, legend = _plot_pie_chart(data, colors, hide_legend=config.vars.cat.redact)
|
|
577
|
+
|
|
578
|
+
else:
|
|
579
|
+
msg = (
|
|
580
|
+
f"'{plot_type}' is not a valid plot type! "
|
|
581
|
+
"Expected values are ['bar', 'pie']"
|
|
582
|
+
)
|
|
583
|
+
msg
|
|
584
|
+
raise ValueError(msg)
|
|
585
|
+
|
|
586
|
+
return plot_360_n0sc0pe(
|
|
587
|
+
config,
|
|
588
|
+
bbox_extra_artists=[] if legend is None else [legend],
|
|
589
|
+
bbox_inches="tight",
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def create_comparison_color_list(config: Settings) -> List[str]:
|
|
594
|
+
colors = config.html.style.primary_colors
|
|
595
|
+
labels = config.html.style._labels
|
|
596
|
+
|
|
597
|
+
if colors < labels:
|
|
598
|
+
init = colors[0]
|
|
599
|
+
end = colors[1] if len(colors) >= 2 else "#000000"
|
|
600
|
+
cmap = LinearSegmentedColormap.from_list("ts_leg", [init, end], len(labels))
|
|
601
|
+
colors = [rgb2hex(cmap(i)) for i in range(cmap.N)]
|
|
602
|
+
return colors
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _format_ts_date_axis(
|
|
606
|
+
series: pd.Series,
|
|
607
|
+
axis: matplotlib.axis.Axis,
|
|
608
|
+
) -> matplotlib.axis.Axis:
|
|
609
|
+
if isinstance(series.index, pd.DatetimeIndex):
|
|
610
|
+
locator = AutoDateLocator()
|
|
611
|
+
axis.xaxis.set_major_locator(locator)
|
|
612
|
+
axis.xaxis.set_major_formatter(ConciseDateFormatter(locator))
|
|
613
|
+
|
|
614
|
+
return axis
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@manage_matplotlib_context()
|
|
618
|
+
def plot_timeseries_gap_analysis(
|
|
619
|
+
config: Settings,
|
|
620
|
+
series: Union[pd.Series, List[pd.Series]],
|
|
621
|
+
gaps: Union[pd.Series, List[pd.Series]],
|
|
622
|
+
figsize: tuple = (6, 3),
|
|
623
|
+
) -> matplotlib.figure.Figure:
|
|
624
|
+
"""Plot an line plot from the data and return the AxesSubplot object.
|
|
625
|
+
Args:
|
|
626
|
+
variables: The data to plot.
|
|
627
|
+
figsize: The size of the figure (width, height) in inches, default (6,4).
|
|
628
|
+
Returns:
|
|
629
|
+
The TimeSeries lineplot.
|
|
630
|
+
"""
|
|
631
|
+
fig = plt.figure(figsize=figsize)
|
|
632
|
+
ax = fig.add_subplot(111)
|
|
633
|
+
|
|
634
|
+
colors = create_comparison_color_list(config)
|
|
635
|
+
if isinstance(series, list):
|
|
636
|
+
min_ = min(s.min() for s in series)
|
|
637
|
+
max_ = max(s.max() for s in series)
|
|
638
|
+
labels = config.html.style._labels
|
|
639
|
+
for serie, gaps_, color, label in zip(series, gaps, colors, labels):
|
|
640
|
+
serie.plot(
|
|
641
|
+
ax=ax,
|
|
642
|
+
label=label,
|
|
643
|
+
color=color,
|
|
644
|
+
alpha=0.65,
|
|
645
|
+
x_compat=True,
|
|
646
|
+
)
|
|
647
|
+
_format_ts_date_axis(serie, ax)
|
|
648
|
+
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
|
|
649
|
+
for gap in gaps_:
|
|
650
|
+
ax.fill_between(x=gap, y1=min_, y2=max_, color=color, alpha=0.25)
|
|
651
|
+
else:
|
|
652
|
+
series.plot(ax=ax, x_compat=True)
|
|
653
|
+
_format_ts_date_axis(series, ax)
|
|
654
|
+
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
|
|
655
|
+
|
|
656
|
+
for gap in gaps:
|
|
657
|
+
ax.fill_between(
|
|
658
|
+
x=gap, y1=series.min(), y2=series.max(), color=colors[0], alpha=0.25
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
return plot_360_n0sc0pe(config)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@manage_matplotlib_context()
|
|
665
|
+
def plot_overview_timeseries(
|
|
666
|
+
config: Settings,
|
|
667
|
+
variables: Any,
|
|
668
|
+
figsize: tuple = (6, 4),
|
|
669
|
+
scale: bool = False,
|
|
670
|
+
) -> matplotlib.figure.Figure:
|
|
671
|
+
"""Plot an line plot from the data and return the AxesSubplot object.
|
|
672
|
+
Args:
|
|
673
|
+
variables: The data to plot.
|
|
674
|
+
figsize: The size of the figure (width, height) in inches, default (6,4).
|
|
675
|
+
scale: Scale series values between [0,1]. Defaults to False.
|
|
676
|
+
Returns:
|
|
677
|
+
The TimeSeries lineplot.
|
|
678
|
+
"""
|
|
679
|
+
fig = plt.figure(figsize=figsize)
|
|
680
|
+
ax = fig.add_subplot(111)
|
|
681
|
+
|
|
682
|
+
col = next(iter(variables))
|
|
683
|
+
if isinstance(variables[col]["type"], list):
|
|
684
|
+
colors = create_comparison_color_list(config)
|
|
685
|
+
line_styles = ["-", "--"]
|
|
686
|
+
for col, data in variables.items():
|
|
687
|
+
if all(iter([t == "TimeSeries" for t in data["type"]])):
|
|
688
|
+
for i, series in enumerate(data["series"]):
|
|
689
|
+
if scale:
|
|
690
|
+
series = (series - series.min()) / (series.max() - series.min())
|
|
691
|
+
series.plot(
|
|
692
|
+
ax=ax,
|
|
693
|
+
label=col,
|
|
694
|
+
linestyle=line_styles[i],
|
|
695
|
+
color=colors[i],
|
|
696
|
+
alpha=0.65,
|
|
697
|
+
)
|
|
698
|
+
else:
|
|
699
|
+
for col, data in variables.items():
|
|
700
|
+
if data["type"] == "TimeSeries":
|
|
701
|
+
series = data["series"]
|
|
702
|
+
if scale:
|
|
703
|
+
series = (series - series.min()) / (series.max() - series.min())
|
|
704
|
+
series.plot(ax=ax, label=col, alpha=0.65)
|
|
705
|
+
|
|
706
|
+
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
|
|
707
|
+
plt.subplots_adjust(right=0.7)
|
|
708
|
+
return plot_360_n0sc0pe(config)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _plot_timeseries(
|
|
712
|
+
config: Settings,
|
|
713
|
+
series: Union[list, pd.Series],
|
|
714
|
+
figsize: tuple = (6, 4),
|
|
715
|
+
) -> matplotlib.figure.Figure:
|
|
716
|
+
"""Plot an line plot from the data and return the AxesSubplot object.
|
|
717
|
+
Args:
|
|
718
|
+
series: The data to plot
|
|
719
|
+
figsize: The size of the figure (width, height) in inches, default (6,4)
|
|
720
|
+
Returns:
|
|
721
|
+
The TimeSeries lineplot.
|
|
722
|
+
"""
|
|
723
|
+
fig = plt.figure(figsize=figsize)
|
|
724
|
+
plot = fig.add_subplot(111)
|
|
725
|
+
|
|
726
|
+
if isinstance(series, list):
|
|
727
|
+
labels = config.html.style._labels
|
|
728
|
+
colors = create_comparison_color_list(config)
|
|
729
|
+
|
|
730
|
+
for serie, color, label in zip(series, colors, labels):
|
|
731
|
+
ax = serie.plot(color=color, label=label, alpha=0.75, x_compat=True)
|
|
732
|
+
_format_ts_date_axis(serie, ax)
|
|
733
|
+
|
|
734
|
+
else:
|
|
735
|
+
ax = series.plot(color=config.html.style.primary_colors[0], x_compat=True)
|
|
736
|
+
_format_ts_date_axis(series, ax)
|
|
737
|
+
|
|
738
|
+
return plot
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
@manage_matplotlib_context()
|
|
742
|
+
def mini_ts_plot(
|
|
743
|
+
config: Settings,
|
|
744
|
+
series: Union[list, pd.Series],
|
|
745
|
+
figsize: Tuple[float, float] = (3, 2.25),
|
|
746
|
+
) -> str:
|
|
747
|
+
"""Plot an time-series plot of the data.
|
|
748
|
+
Args:
|
|
749
|
+
config: profiling settings.
|
|
750
|
+
series: The data to plot.
|
|
751
|
+
figsize: The size of the figure (width, height) in inches, default (3, 2.25)
|
|
752
|
+
Returns:
|
|
753
|
+
The resulting timeseries plot encoded as a string.
|
|
754
|
+
"""
|
|
755
|
+
plot = _plot_timeseries(config, series, figsize=figsize)
|
|
756
|
+
plot.xaxis.set_tick_params(rotation=45)
|
|
757
|
+
plt.rc("ytick", labelsize=3)
|
|
758
|
+
|
|
759
|
+
for tick in plot.xaxis.get_major_ticks():
|
|
760
|
+
if isinstance(series.index, pd.DatetimeIndex):
|
|
761
|
+
tick.label1.set_fontsize(6)
|
|
762
|
+
else:
|
|
763
|
+
tick.label1.set_fontsize(8)
|
|
764
|
+
plot.figure.tight_layout()
|
|
765
|
+
return plot_360_n0sc0pe(config)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _get_ts_lag(config: Settings, series: pd.Series) -> int:
|
|
769
|
+
lag = config.vars.timeseries.pacf_acf_lag
|
|
770
|
+
max_lag_size = (len(series) // 2) - 1
|
|
771
|
+
return np.min([lag, max_lag_size])
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _plot_acf_pacf(
|
|
775
|
+
config: Settings, series: pd.Series, figsize: tuple = (15, 5)
|
|
776
|
+
) -> str:
|
|
777
|
+
color = config.html.style.primary_colors[0]
|
|
778
|
+
|
|
779
|
+
lag = _get_ts_lag(config, series)
|
|
780
|
+
_, axes = plt.subplots(nrows=1, ncols=2, figsize=figsize)
|
|
781
|
+
|
|
782
|
+
plot_acf(
|
|
783
|
+
series.dropna(),
|
|
784
|
+
lags=lag,
|
|
785
|
+
ax=axes[0],
|
|
786
|
+
title="ACF",
|
|
787
|
+
fft=True,
|
|
788
|
+
color=color,
|
|
789
|
+
vlines_kwargs={"colors": color},
|
|
790
|
+
)
|
|
791
|
+
plot_pacf(
|
|
792
|
+
series.dropna(),
|
|
793
|
+
lags=lag,
|
|
794
|
+
ax=axes[1],
|
|
795
|
+
title="PACF",
|
|
796
|
+
method="ywm",
|
|
797
|
+
color=color,
|
|
798
|
+
vlines_kwargs={"colors": color},
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
for ax in axes:
|
|
802
|
+
for item in ax.collections:
|
|
803
|
+
if type(item) is PolyCollection:
|
|
804
|
+
item.set_facecolor(color)
|
|
805
|
+
|
|
806
|
+
return plot_360_n0sc0pe(config)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _plot_acf_pacf_comparison(
|
|
810
|
+
config: Settings, series: List[pd.Series], figsize: tuple = (15, 5)
|
|
811
|
+
) -> str:
|
|
812
|
+
colors = config.html.style.primary_colors
|
|
813
|
+
n_labels = len(config.html.style._labels)
|
|
814
|
+
colors = create_comparison_color_list(config)
|
|
815
|
+
|
|
816
|
+
_, axes = plt.subplots(nrows=n_labels, ncols=2, figsize=figsize)
|
|
817
|
+
is_first = True
|
|
818
|
+
for serie, (acf_axis, pacf_axis), color in zip(series, axes, colors):
|
|
819
|
+
lag = _get_ts_lag(config, serie)
|
|
820
|
+
|
|
821
|
+
plot_acf(
|
|
822
|
+
serie.dropna(),
|
|
823
|
+
lags=lag,
|
|
824
|
+
ax=acf_axis,
|
|
825
|
+
title="ACF" if is_first else "",
|
|
826
|
+
fft=True,
|
|
827
|
+
color=color,
|
|
828
|
+
vlines_kwargs={"colors": color},
|
|
829
|
+
)
|
|
830
|
+
plot_pacf(
|
|
831
|
+
serie.dropna(),
|
|
832
|
+
lags=lag,
|
|
833
|
+
ax=pacf_axis,
|
|
834
|
+
title="PACF" if is_first else "",
|
|
835
|
+
method="ywm",
|
|
836
|
+
color=color,
|
|
837
|
+
vlines_kwargs={"colors": color},
|
|
838
|
+
)
|
|
839
|
+
is_first = False
|
|
840
|
+
|
|
841
|
+
for row, color in zip(axes, colors):
|
|
842
|
+
for ax in row:
|
|
843
|
+
for item in ax.collections:
|
|
844
|
+
if isinstance(item, PolyCollection):
|
|
845
|
+
item.set_facecolor(color)
|
|
846
|
+
|
|
847
|
+
return plot_360_n0sc0pe(config)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
@manage_matplotlib_context()
|
|
851
|
+
def plot_acf_pacf(
|
|
852
|
+
config: Settings, series: Union[list, pd.Series], figsize: tuple = (15, 5)
|
|
853
|
+
) -> str:
|
|
854
|
+
if isinstance(series, list):
|
|
855
|
+
return _plot_acf_pacf_comparison(config, series, figsize)
|
|
856
|
+
else:
|
|
857
|
+
return _plot_acf_pacf(config, series, figsize)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _prepare_heatmap_data(
|
|
861
|
+
dataframe: pd.DataFrame,
|
|
862
|
+
entity_column: str,
|
|
863
|
+
sortby: Optional[Union[str, list]] = None,
|
|
864
|
+
max_entities: int = 5,
|
|
865
|
+
selected_entities: Optional[List[str]] = None,
|
|
866
|
+
) -> pd.DataFrame:
|
|
867
|
+
if sortby is None:
|
|
868
|
+
sortbykey = "_index"
|
|
869
|
+
df = dataframe[entity_column].copy().reset_index()
|
|
870
|
+
df.columns = [sortbykey, entity_column]
|
|
871
|
+
|
|
872
|
+
else:
|
|
873
|
+
if isinstance(sortby, str):
|
|
874
|
+
sortby = [sortby]
|
|
875
|
+
cols = [entity_column, *sortby]
|
|
876
|
+
df = dataframe[cols].copy()
|
|
877
|
+
sortbykey = sortby[0]
|
|
878
|
+
|
|
879
|
+
if df[sortbykey].dtype == "O":
|
|
880
|
+
try:
|
|
881
|
+
df[sortbykey] = pd.to_datetime(df[sortbykey])
|
|
882
|
+
except Exception as ex:
|
|
883
|
+
raise ValueError(
|
|
884
|
+
f"column {sortbykey} dtype {df[sortbykey].dtype} is not supported."
|
|
885
|
+
) from ex
|
|
886
|
+
nbins = np.min([50, df[sortbykey].nunique()])
|
|
887
|
+
|
|
888
|
+
df["__bins"] = pd.cut(
|
|
889
|
+
df[sortbykey], bins=nbins, include_lowest=True, labels=range(nbins)
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
df = df.groupby([entity_column, "__bins"])[sortbykey].count()
|
|
893
|
+
df = (
|
|
894
|
+
df.reset_index()
|
|
895
|
+
.pivot_table(values=sortbykey, index="__bins", columns=entity_column)
|
|
896
|
+
.T
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
if selected_entities:
|
|
900
|
+
df = df[selected_entities]
|
|
901
|
+
else:
|
|
902
|
+
df = df[:max_entities]
|
|
903
|
+
|
|
904
|
+
return df
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _create_timeseries_heatmap(
|
|
908
|
+
df: pd.DataFrame,
|
|
909
|
+
figsize: Tuple[int, int] = (12, 5),
|
|
910
|
+
color: str = "#337ab7",
|
|
911
|
+
) -> plt.Axes:
|
|
912
|
+
_, ax = plt.subplots(figsize=figsize)
|
|
913
|
+
cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
|
|
914
|
+
"report", ["white", color], N=64
|
|
915
|
+
)
|
|
916
|
+
pc = ax.pcolormesh(df, edgecolors=ax.get_facecolor(), linewidth=0.25, cmap=cmap)
|
|
917
|
+
pc.set_clim(0, np.nanmax(df))
|
|
918
|
+
ax.set_yticks([x + 0.5 for x in range(len(df))])
|
|
919
|
+
ax.set_yticklabels(df.index)
|
|
920
|
+
ax.set_xticks([])
|
|
921
|
+
ax.set_xlabel("Time")
|
|
922
|
+
ax.invert_yaxis()
|
|
923
|
+
return ax
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@typechecked
|
|
927
|
+
def timeseries_heatmap(
|
|
928
|
+
dataframe: pd.DataFrame,
|
|
929
|
+
entity_column: str,
|
|
930
|
+
sortby: Optional[Union[str, list]] = None,
|
|
931
|
+
max_entities: int = 5,
|
|
932
|
+
selected_entities: Optional[List[str]] = None,
|
|
933
|
+
figsize: Tuple[int, int] = (12, 5),
|
|
934
|
+
color: str = "#337ab7",
|
|
935
|
+
) -> plt.Axes:
|
|
936
|
+
"""Generate a multi entity timeseries heatmap based on a pandas DataFrame.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
dataframe: the pandas DataFrame
|
|
940
|
+
entity_column: name of the entities column
|
|
941
|
+
sortby: column that define the timesteps (only dates and numerical variables are supported)
|
|
942
|
+
max_entities: max entities that will be displayed
|
|
943
|
+
selected_entities: Optional list of entities to be displayed (overules max_entities)
|
|
944
|
+
figsize: The size of the figure (width, height) in inches, default (10,5)
|
|
945
|
+
color: the primary color, default '#337ab7'
|
|
946
|
+
Returns:
|
|
947
|
+
The TimeSeries heatmap.
|
|
948
|
+
"""
|
|
949
|
+
df = _prepare_heatmap_data(
|
|
950
|
+
dataframe, entity_column, sortby, max_entities, selected_entities
|
|
951
|
+
)
|
|
952
|
+
ax = _create_timeseries_heatmap(df, figsize, color)
|
|
953
|
+
ax.set_aspect(1)
|
|
954
|
+
return ax
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _set_visibility(
|
|
958
|
+
axis: matplotlib.axis.Axis, tick_mark: str = "none"
|
|
959
|
+
) -> matplotlib.axis.Axis:
|
|
960
|
+
for anchor in ["top", "right", "bottom", "left"]:
|
|
961
|
+
axis.spines[anchor].set_visible(False)
|
|
962
|
+
axis.xaxis.set_ticks_position(tick_mark)
|
|
963
|
+
axis.yaxis.set_ticks_position(tick_mark)
|
|
964
|
+
return axis
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def missing_bar(
|
|
968
|
+
notnull_counts: pd.Series,
|
|
969
|
+
nrows: int,
|
|
970
|
+
figsize: Tuple[float, float] = (25, 10),
|
|
971
|
+
fontsize: float = 16,
|
|
972
|
+
labels: bool = True,
|
|
973
|
+
color: Tuple[float, ...] = (0.41, 0.41, 0.41),
|
|
974
|
+
label_rotation: int = 45,
|
|
975
|
+
) -> matplotlib.axis.Axis:
|
|
976
|
+
"""
|
|
977
|
+
A bar chart visualization of the missing data.
|
|
978
|
+
|
|
979
|
+
Inspired by https://github.com/ResidentMario/missingno
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
notnull_counts: Number of nonnull values per column.
|
|
983
|
+
nrows: Number of rows in the dataframe.
|
|
984
|
+
figsize: The size of the figure to display.
|
|
985
|
+
fontsize: The figure's font size. This default to 16.
|
|
986
|
+
labels: Whether or not to display the column names. Would need to be turned off on particularly large
|
|
987
|
+
displays. Defaults to True.
|
|
988
|
+
color: The color of the filled columns. Default to the RGB multiple `(0.25, 0.25, 0.25)`.
|
|
989
|
+
label_rotation: What angle to rotate the text labels to. Defaults to 45 degrees.
|
|
990
|
+
Returns:
|
|
991
|
+
The plot axis.
|
|
992
|
+
"""
|
|
993
|
+
percentage = notnull_counts / nrows
|
|
994
|
+
|
|
995
|
+
if len(notnull_counts) <= 50:
|
|
996
|
+
ax0 = percentage.plot.bar(figsize=figsize, fontsize=fontsize, color=color)
|
|
997
|
+
ax0.set_xticklabels(
|
|
998
|
+
ax0.get_xticklabels(),
|
|
999
|
+
ha="right",
|
|
1000
|
+
fontsize=fontsize,
|
|
1001
|
+
rotation=label_rotation,
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
ax1 = ax0.twiny()
|
|
1005
|
+
ax1.set_xticks(ax0.get_xticks())
|
|
1006
|
+
ax1.set_xlim(ax0.get_xlim())
|
|
1007
|
+
ax1.set_xticklabels(
|
|
1008
|
+
notnull_counts, ha="left", fontsize=fontsize, rotation=label_rotation
|
|
1009
|
+
)
|
|
1010
|
+
else:
|
|
1011
|
+
ax0 = percentage.plot.barh(figsize=figsize, fontsize=fontsize, color=color)
|
|
1012
|
+
ylabels = ax0.get_yticklabels() if labels else []
|
|
1013
|
+
ax0.set_yticklabels(ylabels, fontsize=fontsize)
|
|
1014
|
+
|
|
1015
|
+
ax1 = ax0.twinx()
|
|
1016
|
+
ax1.set_yticks(ax0.get_yticks())
|
|
1017
|
+
ax1.set_ylim(ax0.get_ylim())
|
|
1018
|
+
ax1.set_yticklabels(notnull_counts, fontsize=fontsize)
|
|
1019
|
+
|
|
1020
|
+
for ax in [ax0, ax1]:
|
|
1021
|
+
ax = _set_visibility(ax)
|
|
1022
|
+
|
|
1023
|
+
return ax0
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def missing_matrix(
|
|
1027
|
+
notnull: Any,
|
|
1028
|
+
columns: List[str],
|
|
1029
|
+
height: int,
|
|
1030
|
+
figsize: Tuple[float, float] = (25, 10),
|
|
1031
|
+
color: Tuple[float, ...] = (0.41, 0.41, 0.41),
|
|
1032
|
+
fontsize: float = 16,
|
|
1033
|
+
labels: bool = True,
|
|
1034
|
+
label_rotation: int = 45,
|
|
1035
|
+
) -> matplotlib.axis.Axis:
|
|
1036
|
+
"""
|
|
1037
|
+
A matrix visualization of missing data.
|
|
1038
|
+
|
|
1039
|
+
Inspired by https://github.com/ResidentMario/missingno
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
notnull: Missing data indicator matrix.
|
|
1043
|
+
columns: List of column names.
|
|
1044
|
+
height: Number of rows in the dataframe.
|
|
1045
|
+
figsize: The size of the figure to display.
|
|
1046
|
+
fontsize: The figure's font size. Default to 16.
|
|
1047
|
+
labels: Whether or not to display the column names when there is more than 50 columns.
|
|
1048
|
+
label_rotation: What angle to rotate the text labels to. Defaults to 45 degrees.
|
|
1049
|
+
color: The color of the filled columns. Default is `(0.41, 0.41, 0.41)`.
|
|
1050
|
+
Returns:
|
|
1051
|
+
The plot axis.
|
|
1052
|
+
"""
|
|
1053
|
+
width = len(columns)
|
|
1054
|
+
missing_grid = np.zeros((height, width, 3), dtype=np.float32)
|
|
1055
|
+
|
|
1056
|
+
missing_grid[notnull] = color
|
|
1057
|
+
missing_grid[~notnull] = [1, 1, 1]
|
|
1058
|
+
|
|
1059
|
+
_, ax = plt.subplots(1, 1, figsize=figsize)
|
|
1060
|
+
|
|
1061
|
+
# Create the missing matrix plot.
|
|
1062
|
+
ax.imshow(missing_grid, interpolation="none")
|
|
1063
|
+
ax.set_aspect("auto")
|
|
1064
|
+
ax.grid(False)
|
|
1065
|
+
ax.xaxis.tick_top()
|
|
1066
|
+
|
|
1067
|
+
ha = "left"
|
|
1068
|
+
ax.set_xticks(list(range(0, width)))
|
|
1069
|
+
ax.set_xticklabels(columns, rotation=label_rotation, ha=ha, fontsize=fontsize)
|
|
1070
|
+
ax.set_yticks([0, height - 1])
|
|
1071
|
+
ax.set_yticklabels([1, height], fontsize=fontsize)
|
|
1072
|
+
|
|
1073
|
+
separators = [x + 0.5 for x in range(0, width - 1)]
|
|
1074
|
+
for point in separators:
|
|
1075
|
+
ax.axvline(point, linestyle="-", color="white")
|
|
1076
|
+
|
|
1077
|
+
if not labels and width > 50:
|
|
1078
|
+
ax.set_xticklabels([])
|
|
1079
|
+
|
|
1080
|
+
ax = _set_visibility(ax)
|
|
1081
|
+
return ax
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def missing_heatmap(
|
|
1085
|
+
corr_mat: Any,
|
|
1086
|
+
mask: Any,
|
|
1087
|
+
figsize: Tuple[float, float] = (20, 12),
|
|
1088
|
+
fontsize: float = 16,
|
|
1089
|
+
labels: bool = True,
|
|
1090
|
+
label_rotation: int = 45,
|
|
1091
|
+
cmap: str = "RdBu",
|
|
1092
|
+
normalized_cmap: bool = True,
|
|
1093
|
+
cbar: bool = True,
|
|
1094
|
+
ax: matplotlib.axis.Axis = None,
|
|
1095
|
+
) -> matplotlib.axis.Axis:
|
|
1096
|
+
"""
|
|
1097
|
+
Presents a `seaborn` heatmap visualization of missing data correlation.
|
|
1098
|
+
Note that this visualization has no special support for large datasets.
|
|
1099
|
+
|
|
1100
|
+
Inspired by https://github.com/ResidentMario/missingno
|
|
1101
|
+
|
|
1102
|
+
Args:
|
|
1103
|
+
corr_mat: correlation matrix.
|
|
1104
|
+
mask: Upper-triangle mask.
|
|
1105
|
+
figsize: The size of the figure to display. Defaults to (20, 12).
|
|
1106
|
+
fontsize: The figure's font size.
|
|
1107
|
+
labels: Whether or not to label each matrix entry with its correlation (default is True).
|
|
1108
|
+
label_rotation: What angle to rotate the text labels to. Defaults to 45 degrees.
|
|
1109
|
+
cmap: Which colormap to use. Defaults to `RdBu`.
|
|
1110
|
+
normalized_cmap: Use a normalized colormap threshold or not. Defaults to True
|
|
1111
|
+
Returns:
|
|
1112
|
+
The plot axis.
|
|
1113
|
+
"""
|
|
1114
|
+
_, ax = plt.subplots(1, 1, figsize=figsize)
|
|
1115
|
+
norm_args = {"vmin": -1, "vmax": 1} if normalized_cmap else {}
|
|
1116
|
+
|
|
1117
|
+
if labels:
|
|
1118
|
+
sns.heatmap(
|
|
1119
|
+
corr_mat,
|
|
1120
|
+
mask=mask,
|
|
1121
|
+
cmap=cmap,
|
|
1122
|
+
ax=ax,
|
|
1123
|
+
cbar=cbar,
|
|
1124
|
+
annot=True,
|
|
1125
|
+
annot_kws={"size": fontsize - 2},
|
|
1126
|
+
**norm_args,
|
|
1127
|
+
)
|
|
1128
|
+
else:
|
|
1129
|
+
sns.heatmap(corr_mat, mask=mask, cmap=cmap, ax=ax, cbar=cbar, **norm_args)
|
|
1130
|
+
|
|
1131
|
+
# Apply visual corrections and modifications.
|
|
1132
|
+
ax.xaxis.tick_bottom()
|
|
1133
|
+
ax.set_xticklabels(
|
|
1134
|
+
ax.xaxis.get_majorticklabels(),
|
|
1135
|
+
rotation=label_rotation,
|
|
1136
|
+
ha="right",
|
|
1137
|
+
fontsize=fontsize,
|
|
1138
|
+
)
|
|
1139
|
+
ax.set_yticklabels(ax.yaxis.get_majorticklabels(), rotation=0, fontsize=fontsize)
|
|
1140
|
+
ax = _set_visibility(ax)
|
|
1141
|
+
ax.patch.set_visible(False)
|
|
1142
|
+
|
|
1143
|
+
for text in ax.texts:
|
|
1144
|
+
t = float(text.get_text())
|
|
1145
|
+
if 0.95 <= t < 1:
|
|
1146
|
+
text.set_text("<1")
|
|
1147
|
+
elif -1 < t <= -0.95:
|
|
1148
|
+
text.set_text(">-1")
|
|
1149
|
+
elif t == 1:
|
|
1150
|
+
text.set_text("1")
|
|
1151
|
+
elif t == -1:
|
|
1152
|
+
text.set_text("-1")
|
|
1153
|
+
elif -0.05 < t < 0.05:
|
|
1154
|
+
text.set_text("")
|
|
1155
|
+
else:
|
|
1156
|
+
text.set_text(round(t, 1))
|
|
1157
|
+
|
|
1158
|
+
return ax
|