From e27d410085ebaf9ab98069a5b175d800259d95a3 Mon Sep 17 00:00:00 2001 From: Alexander <47296670+Marsmaennchen221@users.noreply.github.com> Date: Mon, 8 May 2023 22:16:44 +0200 Subject: [PATCH] feat: Added `Table.plot_histograms` to plot a histogram for each column in the table (#252) Closes #157. ### Summary of Changes Added `Table.plot_histograms` to plot a histpogram for each column in the table Changed xticks in `Column.plot_histogram` to match `Table.plot_histograms` (the first and last xtick labels are outside of the value range) --------- Co-authored-by: sibre28 <86068340+sibre28@users.noreply.github.com> Co-authored-by: Lars Reimann --- docs/tutorials/data_visualization.ipynb | 20 ++++++++++++ src/safeds/data/tabular/containers/_table.py | 29 ++++++++++++++++++ .../snapshot_histograms/four_columns.png | Bin 0 -> 24457 bytes .../image/snapshot_histograms/one_column.png | Bin 0 -> 8359 bytes .../containers/_column/test_plot_boxplot.py | 5 ++- .../containers/_column/test_plot_histogram.py | 5 ++- .../_table/test_plot_correlation_heatmap.py | 7 +++-- .../containers/_table/test_plot_histograms.py | 24 +++++++++++++++ .../containers/_table/test_plot_lineplot.py | 5 ++- .../_table/test_plot_scatterplot.py | 5 ++- 10 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 tests/resources/image/snapshot_histograms/four_columns.png create mode 100644 tests/resources/image/snapshot_histograms/one_column.png create mode 100644 tests/safeds/data/tabular/containers/_table/test_plot_histograms.py diff --git a/docs/tutorials/data_visualization.ipynb b/docs/tutorials/data_visualization.ipynb index 739dd1d05..ca1c3ed5f 100644 --- a/docs/tutorials/data_visualization.ipynb +++ b/docs/tutorials/data_visualization.ipynb @@ -210,6 +210,26 @@ } } }, + { + "cell_type": "markdown", + "source": [ + "## Histogram of all columns" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "titanic_numerical.plot_histograms()" + ], + "metadata": { + "collapsed": false + } + }, { "cell_type": "markdown", "source": [ diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index c05d47c1a..747b1b32c 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -1313,6 +1313,35 @@ def plot_scatterplot(self, x_column_name: str, y_column_name: str) -> Image: buffer.seek(0) return Image(buffer, format_=ImageFormat.PNG) + def plot_histograms(self) -> Image: + """ + Plot a histogram for every column. + + Returns + ------- + plot: Image + The plot as an image. + """ + col_wrap = min(self.number_of_columns, 3) + + data = pd.melt(self._data, value_vars=self.column_names) + grid = sns.FacetGrid(data=data, col="variable", col_wrap=col_wrap, sharex=False, sharey=False) + grid.map(sns.histplot, "value") + grid.set_xlabels("") + grid.set_ylabels("") + grid.set_titles("{col_name}") + for axes in grid.axes.flat: + axes.set_xticks(axes.get_xticks()) + axes.set_xticklabels(axes.get_xticklabels(), rotation=45, horizontalalignment="right") + grid.tight_layout() + fig = grid.fig + + buffer = io.BytesIO() + fig.savefig(buffer, format="png") + plt.close() + buffer.seek(0) + return Image(buffer, ImageFormat.PNG) + # ------------------------------------------------------------------------------------------------------------------ # Conversion # ------------------------------------------------------------------------------------------------------------------ diff --git a/tests/resources/image/snapshot_histograms/four_columns.png b/tests/resources/image/snapshot_histograms/four_columns.png new file mode 100644 index 0000000000000000000000000000000000000000..45e050640b173dcfa99485b899d8690536a891ea GIT binary patch literal 24457 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%0DoqTveKVawp(Z`gBr16Uii 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 literal 0 HcmV?d00001 diff --git a/tests/safeds/data/tabular/containers/_column/test_plot_boxplot.py b/tests/safeds/data/tabular/containers/_column/test_plot_boxplot.py index 8d9212f59..84c0afe87 100644 --- a/tests/safeds/data/tabular/containers/_column/test_plot_boxplot.py +++ b/tests/safeds/data/tabular/containers/_column/test_plot_boxplot.py @@ -11,7 +11,10 @@ def test_should_match_snapshot() -> None: table.get_column("A").plot_boxplot() current = table.get_column("A").plot_boxplot() snapshot = Image.from_png_file(resolve_resource_path("./image/snapshot_boxplot.png")) - assert snapshot._image.tobytes() == current._image.tobytes() + + # Inlining the expression into the assert causes pytest to hang if the assertion fails when run from PyCharm. + assertion = snapshot._image.tobytes() == current._image.tobytes() + assert assertion def test_should_raise_if_column_contains_non_numerical_values() -> None: diff --git a/tests/safeds/data/tabular/containers/_column/test_plot_histogram.py b/tests/safeds/data/tabular/containers/_column/test_plot_histogram.py index 0ba8e1c33..fe4157b6a 100644 --- a/tests/safeds/data/tabular/containers/_column/test_plot_histogram.py +++ b/tests/safeds/data/tabular/containers/_column/test_plot_histogram.py @@ -8,7 +8,10 @@ def test_should_match_snapshot_numeric() -> None: table = Table({"A": [1, 2, 3]}) current = table.get_column("A").plot_histogram() snapshot = Image.from_png_file(resolve_resource_path("./image/snapshot_histogram_numeric.png")) - assert snapshot._image.tobytes() == current._image.tobytes() + + # Inlining the expression into the assert causes pytest to hang if the assertion fails when run from PyCharm. + assertion = snapshot._image.tobytes() == current._image.tobytes() + assert assertion def test_should_match_snapshot_str() -> None: diff --git a/tests/safeds/data/tabular/containers/_table/test_plot_correlation_heatmap.py b/tests/safeds/data/tabular/containers/_table/test_plot_correlation_heatmap.py index 6fa0e1da2..5ba397c68 100644 --- a/tests/safeds/data/tabular/containers/_table/test_plot_correlation_heatmap.py +++ b/tests/safeds/data/tabular/containers/_table/test_plot_correlation_heatmap.py @@ -7,5 +7,8 @@ def test_should_match_snapshot() -> None: table = Table({"A": [1, 2, 3.5], "B": [0.2, 4, 77]}) current = table.plot_correlation_heatmap() - legacy = Image.from_png_file(resolve_resource_path("./image/snapshot_heatmap.png")) - assert legacy._image.tobytes() == current._image.tobytes() + snapshot = Image.from_png_file(resolve_resource_path("./image/snapshot_heatmap.png")) + + # Inlining the expression into the assert causes pytest to hang if the assertion fails when run from PyCharm. + assertion = snapshot._image.tobytes() == current._image.tobytes() + assert assertion diff --git a/tests/safeds/data/tabular/containers/_table/test_plot_histograms.py b/tests/safeds/data/tabular/containers/_table/test_plot_histograms.py new file mode 100644 index 000000000..16572de52 --- /dev/null +++ b/tests/safeds/data/tabular/containers/_table/test_plot_histograms.py @@ -0,0 +1,24 @@ +import pytest +from safeds.data.image.containers import Image +from safeds.data.tabular.containers import Table + +from tests.helpers import resolve_resource_path + + +@pytest.mark.parametrize( + ("table", "path"), + [ + (Table({"A": [1, 2, 3]}), "./image/snapshot_histograms/one_column.png"), + ( + Table({"A": [1, 2, 3], "B": ["A", "A", "Bla"], "C": [True, True, False], "D": [1.0, 2.1, 4.5]}), + "./image/snapshot_histograms/four_columns.png", + ), + ], +) +def test_should_match_snapshot(table: Table, path: str) -> None: + current = table.plot_histograms() + snapshot = Image.from_png_file(resolve_resource_path(path)) + + # Inlining the expression into the assert causes pytest to hang if the assertion fails when run from PyCharm. + assertion = snapshot._image.tobytes() == current._image.tobytes() + assert assertion diff --git a/tests/safeds/data/tabular/containers/_table/test_plot_lineplot.py b/tests/safeds/data/tabular/containers/_table/test_plot_lineplot.py index a370c3146..480d5b147 100644 --- a/tests/safeds/data/tabular/containers/_table/test_plot_lineplot.py +++ b/tests/safeds/data/tabular/containers/_table/test_plot_lineplot.py @@ -10,7 +10,10 @@ def test_should_match_snapshot() -> None: table = Table({"A": [1, 2, 3], "B": [2, 4, 7]}) current = table.plot_lineplot("A", "B") snapshot = Image.from_png_file(resolve_resource_path("./image/snapshot_lineplot.png")) - assert snapshot._image.tobytes() == current._image.tobytes() + + # Inlining the expression into the assert causes pytest to hang if the assertion fails when run from PyCharm. + assertion = snapshot._image.tobytes() == current._image.tobytes() + assert assertion @pytest.mark.parametrize( diff --git a/tests/safeds/data/tabular/containers/_table/test_plot_scatterplot.py b/tests/safeds/data/tabular/containers/_table/test_plot_scatterplot.py index 0ff0ed090..f23353517 100644 --- a/tests/safeds/data/tabular/containers/_table/test_plot_scatterplot.py +++ b/tests/safeds/data/tabular/containers/_table/test_plot_scatterplot.py @@ -10,7 +10,10 @@ def test_should_match_snapshot() -> None: table = Table({"A": [1, 2, 3], "B": [2, 4, 7]}) current = table.plot_scatterplot("A", "B") snapshot = Image.from_png_file(resolve_resource_path("./image/snapshot_scatterplot.png")) - assert snapshot._image.tobytes() == current._image.tobytes() + + # Inlining the expression into the assert causes pytest to hang if the assertion fails when run from PyCharm. + assertion = snapshot._image.tobytes() == current._image.tobytes() + assert assertion @pytest.mark.parametrize(