From 409cb043c854a8b0dd19b7b352f6c1bf539d793a Mon Sep 17 00:00:00 2001 From: stevedodson Date: Wed, 29 Jan 2020 07:07:56 +0000 Subject: [PATCH] Refactoring of plotting + fixes for multiple charts (#117) * Refactoring of plotting + fixes for multiple charts Updates to plotting inline with pandas 0.25.3 Enables plotting of multiple histograms on the same figure. * Fix to setup.py to allow submodules + reformat of code and better Series.hist docs --- docs/source/examples/demo_notebook.ipynb | 10 +- .../examples/online_retail_analysis.ipynb | 18 +-- docs/source/index.rst | 1 - .../reference/api/eland-Series-hist-1.png | Bin 45659 -> 0 bytes .../reference/api/eland-Series-hist-2.png | Bin 0 -> 16303 bytes .../reference/api/eland.Series.hist.rst | 2 +- eland/__init__.py | 1 - eland/field_mappings.py | 3 +- eland/plotting.py | 133 ------------------ eland/plotting/__init__.py | 28 ++++ eland/plotting/_core.py | 127 +++++++++++++++++ eland/plotting/_matplotlib/__init__.py | 40 ++++++ eland/plotting/_matplotlib/hist.py | 131 +++++++++++++++++ eland/query.py | 1 - eland/query_compiler.py | 3 +- eland/series.py | 4 +- .../test_scripted_fields_pytest.py | 1 - .../plotting/test_dataframe_hist_pytest.py | 3 +- .../tests/plotting/test_series_hist_pytest.py | 32 ++++- eland/tests/series/test_hist_pytest.py | 2 +- setup.py | 4 +- 21 files changed, 379 insertions(+), 165 deletions(-) delete mode 100644 docs/source/reference/api/eland-Series-hist-1.png create mode 100644 docs/source/reference/api/eland-Series-hist-2.png delete mode 100644 eland/plotting.py create mode 100644 eland/plotting/__init__.py create mode 100644 eland/plotting/_core.py create mode 100644 eland/plotting/_matplotlib/__init__.py create mode 100644 eland/plotting/_matplotlib/hist.py diff --git a/docs/source/examples/demo_notebook.ipynb b/docs/source/examples/demo_notebook.ipynb index bc9a13c..4fe6ed4 100644 --- a/docs/source/examples/demo_notebook.ipynb +++ b/docs/source/examples/demo_notebook.ipynb @@ -753,7 +753,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 17, @@ -2707,7 +2707,7 @@ " 410.008918\n", " 2470.545974\n", " ...\n", - " 251.698552\n", + " 251.682199\n", " 1.000000\n", " \n", " \n", @@ -2724,7 +2724,7 @@ " 9735.660463\n", " ...\n", " 720.534532\n", - " 4.254967\n", + " 4.288079\n", " \n", " \n", " max\n", @@ -2745,9 +2745,9 @@ "mean 628.253689 7092.142457 ... 511.127842 2.835975\n", "std 266.386661 4578.263193 ... 334.741135 1.939365\n", "min 100.020531 0.000000 ... 0.000000 0.000000\n", - "25% 410.008918 2470.545974 ... 251.698552 1.000000\n", + "25% 410.008918 2470.545974 ... 251.682199 1.000000\n", "50% 640.387285 7612.072403 ... 503.148975 3.000000\n", - "75% 842.233478 9735.660463 ... 720.534532 4.254967\n", + "75% 842.233478 9735.660463 ... 720.534532 4.288079\n", "max 1199.729004 19881.482422 ... 1902.901978 6.000000\n", "\n", "[8 rows x 7 columns]" diff --git a/docs/source/examples/online_retail_analysis.ipynb b/docs/source/examples/online_retail_analysis.ipynb index a8ed6c5..c66322d 100644 --- a/docs/source/examples/online_retail_analysis.ipynb +++ b/docs/source/examples/online_retail_analysis.ipynb @@ -1023,21 +1023,21 @@ " \n", " \n", " 25%\n", - " 14215.123301\n", + " 14221.960201\n", " 1.000000\n", - " 1.250100\n", + " 1.250000\n", " \n", " \n", " 50%\n", - " 15654.828552\n", + " 15671.712170\n", " 2.000000\n", " 2.510000\n", " \n", " \n", " 75%\n", - " 17218.003301\n", - " 6.570576\n", - " 4.210000\n", + " 17214.376367\n", + " 6.615042\n", + " 4.210533\n", " \n", " \n", " max\n", @@ -1055,9 +1055,9 @@ "mean 15590.776680 7.464000 4.103233\n", "std 1764.025160 85.924387 20.104873\n", "min 12347.000000 -9360.000000 0.000000\n", - "25% 14215.123301 1.000000 1.250100\n", - "50% 15654.828552 2.000000 2.510000\n", - "75% 17218.003301 6.570576 4.210000\n", + "25% 14221.960201 1.000000 1.250000\n", + "50% 15671.712170 2.000000 2.510000\n", + "75% 17214.376367 6.615042 4.210533\n", "max 18239.000000 2880.000000 950.990000" ] }, diff --git a/docs/source/index.rst b/docs/source/index.rst index 08b7525..8696873 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -34,7 +34,6 @@ In general, the data resides in elasticsearch and not in memory, which allows el * :doc:`reference/general_utility_functions` * :doc:`reference/dataframe` * :doc:`reference/series` - * :doc:`reference/index` * :doc:`reference/indexing` * :doc:`implementation/index` diff --git a/docs/source/reference/api/eland-Series-hist-1.png b/docs/source/reference/api/eland-Series-hist-1.png deleted file mode 100644 index d1c0465a82b7a3c984b560e51fa38b520d86a667..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45659 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%0Di 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: {'category.keyword': 'category', 'currency': 'currency', ... - return OrderedDict(aggregatables[['aggregatable_es_field_name', 'es_field_name']].to_dict(orient='split')['data']) + return OrderedDict( + aggregatables[['aggregatable_es_field_name', 'es_field_name']].to_dict(orient='split')['data']) def date_field_format(self, es_field_name): """ diff --git a/eland/plotting.py b/eland/plotting.py deleted file mode 100644 index 3c34132..0000000 --- a/eland/plotting.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2019 Elasticsearch BV -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np - -import pandas.core.common as com -from pandas.core.dtypes.generic import ( - ABCIndexClass) -from pandas.plotting._matplotlib.tools import _flatten, _set_ticks_props, _subplots - - -def ed_hist_frame(ed_df, column=None, by=None, grid=True, xlabelsize=None, - xrot=None, ylabelsize=None, yrot=None, ax=None, sharex=False, - sharey=False, figsize=None, layout=None, bins=10, **kwds): - """ - See :pandas_api_docs:`pandas.DataFrame.hist` for usage. - - Notes - ----- - Derived from ``pandas.plotting._core.hist_frame 0.24.2`` - TODO update to ``0.25.1`` - - Ideally, we'd call `hist_frame` directly with histogram data, - but weights are applied to ALL series. For example, we can - plot a histogram of pre-binned data via: - - .. code-block:: python - - counts, bins = np.histogram(data) - plt.hist(bins[:-1], bins, weights=counts) - - However, - - .. code-block:: python - - ax.hist(data[col].dropna().values, bins=bins, **kwds) - - is for ``[col]`` and weights are a single array. - - Examples - -------- - >>> df = ed.DataFrame('localhost', 'flights') - >>> hist = df.select_dtypes(include=[np.number]).hist(figsize=[10,10]) # doctest: +SKIP - """ - # Start with empty pandas data frame derived from - ed_df_bins, ed_df_weights = ed_df._hist(num_bins=bins) - - if by is not None: - raise NotImplementedError("TODO") - - if column is not None: - if not isinstance(column, (list, np.ndarray, ABCIndexClass)): - column = [column] - ed_df_bins = ed_df_bins[column] - ed_df_weights = ed_df_weights[column] - naxes = len(ed_df_bins.columns) - - fig, axes = _subplots(naxes=naxes, ax=ax, squeeze=False, - sharex=sharex, sharey=sharey, figsize=figsize, - layout=layout) - _axes = _flatten(axes) - - for i, col in enumerate(com.try_sort(ed_df_bins.columns)): - ax = _axes[i] - - # pandas code - # pandas / plotting / _core.py: 2410 - # ax.hist(data[col].dropna().values, bins=bins, **kwds) - - ax.hist(ed_df_bins[col][:-1], bins=ed_df_bins[col], weights=ed_df_weights[col], **kwds) - ax.set_title(col) - ax.grid(grid) - - _set_ticks_props(axes, xlabelsize=xlabelsize, xrot=xrot, - ylabelsize=ylabelsize, yrot=yrot) - fig.subplots_adjust(wspace=0.3, hspace=0.3) - - return axes - -def ed_hist_series(ed_s, column=None, by=None, grid=True, xlabelsize=None, - xrot=None, ylabelsize=None, yrot=None, ax=None, - figsize=None, layout=None, bins=10, **kwds): - """ - See :pandas_api_docs:`pandas.Series.hist` for usage. - - Notes - ----- - Derived from ``pandas.plotting._core.hist_frame 0.24.2`` - TODO update to ``0.25.1`` - - - Examples - -------- - >>> df = ed.DataFrame('localhost', 'ecommerce') - >>> hist = df['taxful_total_price'].hist(figsize=[10,10]) # doctest: +SKIP - """ - # this is mostly the same code as above, it has been split out - # to a series specific method now so we can expand series plotting - - - # Start with empty pandas data frame derived from - ed_s_bins, ed_s_weights = ed_s._hist(num_bins=bins) - - if by is not None: - raise NotImplementedError("TODO") - - # raise error rather than warning when series is not plottable - if ed_s_bins.empty: - raise ValueError("{} has no meaningful histogram interval. All values 0." - .format(ed_s.name)) - - naxes = len(ed_s_bins.columns) - fig, axes = _subplots(naxes=naxes, ax=ax, squeeze=False, figsize=figsize, layout=layout) - _axes = _flatten(axes) - for i, col in enumerate(com.try_sort(ed_s_bins.columns)): - ax = _axes[i] - ax.hist(ed_s_bins[col][:-1], bins=ed_s_bins[col], weights=ed_s_weights[col], **kwds) - ax.grid(grid) - - _set_ticks_props(axes, xlabelsize=xlabelsize, xrot=xrot, - ylabelsize=ylabelsize, yrot=yrot) - fig.subplots_adjust(wspace=0.3, hspace=0.3) - - return axes \ No newline at end of file diff --git a/eland/plotting/__init__.py b/eland/plotting/__init__.py new file mode 100644 index 0000000..9176b31 --- /dev/null +++ b/eland/plotting/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2019 Elasticsearch BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Public plotting API + +Based from https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/plotting/__init__.py +but only supporting a subset of plotting methods (for now). +""" +from eland.plotting._core import ( + ed_hist_frame, + ed_hist_series, +) + +__all___ = [ + "ed_hist_frame", + "ed_hist_series", +] diff --git a/eland/plotting/_core.py b/eland/plotting/_core.py new file mode 100644 index 0000000..f3844ac --- /dev/null +++ b/eland/plotting/_core.py @@ -0,0 +1,127 @@ +# Copyright 2019 Elasticsearch BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from eland.plotting._matplotlib.hist import hist_series, hist_frame + + +def ed_hist_series( + self, + by=None, + ax=None, + grid=True, + xlabelsize=None, + xrot=None, + ylabelsize=None, + yrot=None, + figsize=None, + bins=10, + **kwds +): + """ + Draw histogram of the input series using matplotlib. + + See :pandas_api_docs:`pandas.Series.hist` for usage. + + Notes + ----- + Derived from ``pandas.plotting._core.hist_frame 0.25.3`` + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> df = ed.DataFrame('localhost', 'flights') + >>> df[df.OriginWeather == 'Sunny']['FlightTimeMin'].hist(alpha=0.5, density=True) # doctest: +SKIP + >>> df[df.OriginWeather != 'Sunny']['FlightTimeMin'].hist(alpha=0.5, density=True) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + + """ + return hist_series( + self, + by=by, + ax=ax, + grid=grid, + xlabelsize=xlabelsize, + xrot=xrot, + ylabelsize=ylabelsize, + yrot=yrot, + figsize=figsize, + bins=bins, + **kwds + ) + + +def ed_hist_frame( + data, + column=None, + by=None, + grid=True, + xlabelsize=None, + xrot=None, + ylabelsize=None, + yrot=None, + ax=None, + sharex=False, + sharey=False, + figsize=None, + layout=None, + bins=10, + **kwds +): + """ + Make a histogram of the DataFrame's. + + See :pandas_api_docs:`pandas.DataFrame.hist` for usage. + + Notes + ----- + Derived from ``pandas.plotting._core.hist_frame 0.25.3`` + + Ideally, we'd call the pandas method `hist_frame` directly + with histogram data, but weights are applied to ALL series. + For example, we can plot a histogram of pre-binned data via: + + .. code-block:: python + + counts, bins = np.histogram(data) + plt.hist(bins[:-1], bins, weights=counts) + + However, + + .. code-block:: python + + ax.hist(data[col].dropna().values, bins=bins, **kwds) + + is for ``[col]`` and weights are a single array. + + Examples + -------- + >>> df = ed.DataFrame('localhost', 'flights') + >>> hist = df.select_dtypes(include=[np.number]).hist(figsize=[10,10]) # doctest: +SKIP + """ + return hist_frame( + data, + column=column, + by=by, + grid=grid, + xlabelsize=xlabelsize, + xrot=xrot, + ylabelsize=ylabelsize, + yrot=yrot, + ax=ax, + sharex=sharex, + sharey=sharey, + figsize=figsize, + layout=layout, + bins=bins, + **kwds + ) diff --git a/eland/plotting/_matplotlib/__init__.py b/eland/plotting/_matplotlib/__init__.py new file mode 100644 index 0000000..edade50 --- /dev/null +++ b/eland/plotting/_matplotlib/__init__.py @@ -0,0 +1,40 @@ +# Copyright 2019 Elasticsearch BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Public plotting API + +Based from https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/plotting/__init__.py +but only supporting a subset of plotting methods (for now). +""" +from eland.plotting._matplotlib.hist import ( + hist_frame, + hist_series, +) + +__all___ = [ + "hist_frame", + "hist_series", +] diff --git a/eland/plotting/_matplotlib/hist.py b/eland/plotting/_matplotlib/hist.py new file mode 100644 index 0000000..31819d2 --- /dev/null +++ b/eland/plotting/_matplotlib/hist.py @@ -0,0 +1,131 @@ +# Copyright 2019 Elasticsearch BV +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas.core.common as com +from pandas.core.dtypes.generic import ABCIndexClass +from pandas.plotting._matplotlib import converter +from pandas.plotting._matplotlib.tools import _flatten, _set_ticks_props, _subplots + + +def hist_series( + self, + by=None, + ax=None, + grid=True, + xlabelsize=None, + xrot=None, + ylabelsize=None, + yrot=None, + figsize=None, + bins=10, + **kwds +): + import matplotlib.pyplot as plt + + if by is None: + if kwds.get("layout", None) is not None: + raise ValueError( + "The 'layout' keyword is not supported when " "'by' is None" + ) + # hack until the plotting interface is a bit more unified + fig = kwds.pop( + "figure", plt.gcf() if plt.get_fignums() else plt.figure(figsize=figsize) + ) + if figsize is not None and tuple(figsize) != tuple(fig.get_size_inches()): + fig.set_size_inches(*figsize, forward=True) + if ax is None: + ax = fig.gca() + elif ax.get_figure() != fig: + raise AssertionError("passed axis not bound to passed figure") + + self_bins, self_weights = self._hist(num_bins=bins) + # As this is a series, squeeze Series to arrays + self_bins = self_bins.squeeze() + self_weights = self_weights.squeeze() + + ax.hist(self_bins[:-1], bins=self_bins, weights=self_weights, **kwds) + ax.grid(grid) + axes = np.array([ax]) + + _set_ticks_props( + axes, xlabelsize=xlabelsize, xrot=xrot, ylabelsize=ylabelsize, yrot=yrot + ) + + else: + raise NotImplementedError("TODO") + + if hasattr(axes, "ndim"): + if axes.ndim == 1 and len(axes) == 1: + return axes[0] + return axes + + +def hist_frame( + data, + column=None, + by=None, + grid=True, + xlabelsize=None, + xrot=None, + ylabelsize=None, + yrot=None, + ax=None, + sharex=False, + sharey=False, + figsize=None, + layout=None, + bins=10, + **kwds +): + # Start with empty pandas data frame derived from + ed_df_bins, ed_df_weights = data._hist(num_bins=bins) + + converter._WARN = False # no warning for pandas plots + if by is not None: + raise NotImplementedError("TODO") + + if column is not None: + if not isinstance(column, (list, np.ndarray, ABCIndexClass)): + column = [column] + ed_df_bins = ed_df_bins[column] + ed_df_weights = ed_df_weights[column] + naxes = len(ed_df_bins.columns) + + if naxes == 0: + raise ValueError("hist method requires numerical columns, " "nothing to plot.") + + fig, axes = _subplots( + naxes=naxes, + ax=ax, + squeeze=False, + sharex=sharex, + sharey=sharey, + figsize=figsize, + layout=layout, + ) + _axes = _flatten(axes) + + for i, col in enumerate(com.try_sort(data.columns)): + ax = _axes[i] + ax.hist(ed_df_bins[col][:-1], bins=ed_df_bins[col], weights=ed_df_weights[col], **kwds) + ax.set_title(col) + ax.grid(grid) + + _set_ticks_props( + axes, xlabelsize=xlabelsize, xrot=xrot, ylabelsize=ylabelsize, yrot=yrot + ) + fig.subplots_adjust(wspace=0.3, hspace=0.3) + + return axes diff --git a/eland/query.py b/eland/query.py index f120da7..1e12bf9 100644 --- a/eland/query.py +++ b/eland/query.py @@ -149,7 +149,6 @@ class Query: if interval != 0: self._aggs[name] = agg - def to_search_body(self): if self._query.empty(): if self._aggs: diff --git a/eland/query_compiler.py b/eland/query_compiler.py index ed6f04b..6dd3cdf 100644 --- a/eland/query_compiler.py +++ b/eland/query_compiler.py @@ -732,11 +732,13 @@ def elasticsearch_date_to_pandas_date(value: Union[int, str], date_format: str) # TODO investigate how we could generate this just once for a bulk read. return pd.to_datetime(value) + class FieldMappingCache: """ Very simple dict cache for field mappings. This improves performance > 3 times on large datasets as DataFrame access is slower than dict access. """ + def __init__(self, mappings): self._mappings = mappings @@ -764,4 +766,3 @@ class FieldMappingCache: self._date_field_format[es_field_name] = es_date_field_format return es_date_field_format - diff --git a/eland/series.py b/eland/series.py index aa358e2..cc22845 100644 --- a/eland/series.py +++ b/eland/series.py @@ -37,11 +37,11 @@ import numpy as np import pandas as pd from pandas.io.common import _expand_user, _stringify_path +import eland.plotting from eland import NDFrame from eland.arithmetics import ArithmeticSeries, ArithmeticString, ArithmeticNumber from eland.common import DEFAULT_NUM_ROWS_DISPLAYED, docstring_parameter from eland.filter import NotFilter, Equal, Greater, Less, GreaterEqual, LessEqual, ScriptFilter, IsIn -import eland.plotting as gfx def _get_method_name(): @@ -108,7 +108,7 @@ class Series(NDFrame): index_field=index_field, query_compiler=query_compiler) - hist = gfx.ed_hist_series + hist = eland.plotting.ed_hist_series @property def empty(self): diff --git a/eland/tests/field_mappings/test_scripted_fields_pytest.py b/eland/tests/field_mappings/test_scripted_fields_pytest.py index a728038..b5cc248 100644 --- a/eland/tests/field_mappings/test_scripted_fields_pytest.py +++ b/eland/tests/field_mappings/test_scripted_fields_pytest.py @@ -19,7 +19,6 @@ import numpy as np import eland as ed from eland.tests import FLIGHTS_INDEX_NAME, ES_TEST_CLIENT - from eland.tests.common import TestData diff --git a/eland/tests/plotting/test_dataframe_hist_pytest.py b/eland/tests/plotting/test_dataframe_hist_pytest.py index c88734c..4e0a4c4 100644 --- a/eland/tests/plotting/test_dataframe_hist_pytest.py +++ b/eland/tests/plotting/test_dataframe_hist_pytest.py @@ -39,6 +39,7 @@ def test_plot_hist(fig_test, fig_ref): ed_ax = fig_test.subplots() ed_flights.hist(ax=ed_ax) + @check_figures_equal(extensions=['png']) def test_plot_filtered_hist(fig_test, fig_ref): test_data = TestData() @@ -49,8 +50,6 @@ def test_plot_filtered_hist(fig_test, fig_ref): pd_flights = pd_flights[pd_flights.FlightDelayMin > 0] ed_flights = ed_flights[ed_flights.FlightDelayMin > 0] - print(ed_flights.head()) - # This throws a userwarning # (https://github.com/pandas-dev/pandas/blob/171c71611886aab8549a8620c5b0071a129ad685/pandas/plotting/_matplotlib/tools.py#L222) with pytest.warns(UserWarning): diff --git a/eland/tests/plotting/test_series_hist_pytest.py b/eland/tests/plotting/test_series_hist_pytest.py index 9ec4e64..c6ad7bf 100644 --- a/eland/tests/plotting/test_series_hist_pytest.py +++ b/eland/tests/plotting/test_series_hist_pytest.py @@ -25,8 +25,32 @@ def test_plot_hist(fig_test, fig_ref): pd_flights = test_data.pd_flights()['FlightDelayMin'] ed_flights = test_data.ed_flights()['FlightDelayMin'] - pd_ax = fig_ref.subplots() - ed_ax = fig_test.subplots() + pd_flights.hist(figure=fig_ref) + ed_flights.hist(figure=fig_test) - pd_flights.hist(ax=pd_ax) - ed_flights.hist(ax=ed_ax) + +@check_figures_equal(extensions=['png']) +def test_plot_multiple_hists(fig_test, fig_ref): + test_data = TestData() + + pd_flights = test_data.pd_flights() + ed_flights = test_data.ed_flights() + + pd_flights[pd_flights.AvgTicketPrice < 250]['FlightDelayMin'].hist(figure=fig_ref, alpha=0.5, density=True) + pd_flights[pd_flights.AvgTicketPrice > 250]['FlightDelayMin'].hist(figure=fig_ref, alpha=0.5, density=True) + + ed_flights[ed_flights.AvgTicketPrice < 250]['FlightDelayMin'].hist(figure=fig_test, alpha=0.5, density=True) + ed_flights[ed_flights.AvgTicketPrice > 250]['FlightDelayMin'].hist(figure=fig_test, alpha=0.5, density=True) + +@check_figures_equal(extensions=['png']) +def test_plot_multiple_hists_pretty(fig_test, fig_ref): + test_data = TestData() + + pd_flights = test_data.pd_flights() + ed_flights = test_data.ed_flights() + + pd_flights[pd_flights.OriginWeather == 'Sunny']['FlightTimeMin'].hist(figure=fig_ref, alpha=0.5, density=True) + pd_flights[pd_flights.OriginWeather != 'Sunny']['FlightTimeMin'].hist(figure=fig_ref, alpha=0.5, density=True) + + ed_flights[ed_flights.OriginWeather == 'Sunny']['FlightTimeMin'].hist(figure=fig_test, alpha=0.5, density=True) + ed_flights[ed_flights.OriginWeather != 'Sunny']['FlightTimeMin'].hist(figure=fig_test, alpha=0.5, density=True) diff --git a/eland/tests/series/test_hist_pytest.py b/eland/tests/series/test_hist_pytest.py index 7c84553..576c81e 100644 --- a/eland/tests/series/test_hist_pytest.py +++ b/eland/tests/series/test_hist_pytest.py @@ -16,8 +16,8 @@ import numpy as np import pandas as pd -from pandas.util.testing import assert_almost_equal import pytest +from pandas.util.testing import assert_almost_equal from eland.tests.common import TestData diff --git a/setup.py b/setup.py index 48b7150..aad92ec 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ from codecs import open from os import path -from setuptools import setup +from setuptools import setup, find_packages here = path.abspath(path.dirname(__file__)) about = {} @@ -183,7 +183,7 @@ setup( license='Apache 2.0', classifiers=CLASSIFIERS, keywords='elastic eland pandas python', - packages=['eland'], + packages=find_packages(include=["eland", "eland.*"]), install_requires=[ 'elasticsearch>=7.0.5', 'pandas==0.25.3',