From 56982f9d725cdd3901b3f81a3f18c59127abd5d4 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 19 May 2020 14:37:30 -0400 Subject: [PATCH] Centralize and modify variable type inference (#2084) * Centralize and modify variable type inference * Use core.variable_type elsewhere in the library * Bump pinned pandas to avoid bug * Test coverage * Move orientation inference to core and improve error handling * Parameterize necessity of numeric variable by plot type * Tweak language * Shorten parameter name * Correct comment * Tweak docs --- doc/installing.rst | 10 +- doc/releases/v0.11.0.txt | 4 + seaborn/axisgrid.py | 8 +- seaborn/categorical.py | 89 +++++++---------- seaborn/conftest.py | 1 + seaborn/core.py | 152 +++++++++++++++++++++++++++++- seaborn/relational.py | 70 ++++++-------- seaborn/tests/test_categorical.py | 29 +----- seaborn/tests/test_core.py | 79 ++++++++++++++++ seaborn/tests/test_relational.py | 2 +- seaborn/utils.py | 10 +- setup.py | 8 +- testing/deps_pinned.txt | 10 +- 13 files changed, 321 insertions(+), 151 deletions(-) diff --git a/doc/installing.rst b/doc/installing.rst index fd5eee0762..ad7e7a5dfe 100644 --- a/doc/installing.rst +++ b/doc/installing.rst @@ -34,18 +34,18 @@ Dependencies Mandatory dependencies ^^^^^^^^^^^^^^^^^^^^^^ -- `numpy `__ (>= 1.13.3) +- `numpy `__ -- `scipy `__ (>= 1.0.1) +- `scipy `__ -- `pandas `__ (>= 0.22.0) +- `pandas `__ -- `matplotlib `__ (>= 2.1.2) +- `matplotlib `__ Recommended dependencies ^^^^^^^^^^^^^^^^^^^^^^^^ -- `statsmodels `__ (>= 0.8.0) +- `statsmodels `__ Bugs ~~~~ diff --git a/doc/releases/v0.11.0.txt b/doc/releases/v0.11.0.txt index 417b18f11f..8aff7627dd 100644 --- a/doc/releases/v0.11.0.txt +++ b/doc/releases/v0.11.0.txt @@ -23,3 +23,7 @@ v0.11.0 (Unreleased) - Made :meth:`FacetGrid.set_axis_labels` clear labels from "interior" axes. GH2046 - Improved :meth:`FacetGrid.set_titles` with `margin titles=True`, such that texts representing the original row titles are removed before adding new ones. GH2083 + +- Changed how functions that use different representations for numeric and categorical data handle vectors with an `object` data type. Previously, data was considered numeric if it could be coerced to a float representation without error. Now, object-typed vectors are considered numeric only when their contents are themselves numeric. As a consequence, numbers that are encoded as strings will now be treated as categorical data. GH2084 + +- Improved the error messages produced when categorical plots process the orientation parameter. diff --git a/seaborn/axisgrid.py b/seaborn/axisgrid.py index 0cc44b58bf..3eb8c4817b 100644 --- a/seaborn/axisgrid.py +++ b/seaborn/axisgrid.py @@ -10,6 +10,7 @@ import matplotlib.pyplot as plt from . import utils +from .core import variable_type from .palettes import color_palette, blend_palette from .distributions import distplot, kdeplot, _freedman_diaconis_bins from ._decorators import _deprecate_positional_args @@ -1611,15 +1612,10 @@ def _add_axis_labels(self): def _find_numeric_cols(self, data): """Find which variables in a DataFrame are numeric.""" - # This can't be the best way to do this, but I do not - # know what the best way might be, so this seems ok numeric_cols = [] for col in data: - try: - data[col].astype(np.float) + if variable_type(data[col]) == "numeric": numeric_cols.append(col) - except (ValueError, TypeError): - pass return numeric_cols diff --git a/seaborn/categorical.py b/seaborn/categorical.py index 449fd7aa62..94394cb04c 100644 --- a/seaborn/categorical.py +++ b/seaborn/categorical.py @@ -11,6 +11,7 @@ from distutils.version import LooseVersion from . import utils +from .core import variable_type, infer_orient from .utils import iqr, categorical_order, remove_na from .algorithms import bootstrap from .palettes import color_palette, husl_palette, light_palette, dark_palette @@ -30,6 +31,7 @@ class _CategoricalPlotter(object): width = .8 default_palette = "light" + require_numeric = True def establish_variables(self, x=None, y=None, hue=None, data=None, orient=None, order=None, hue_order=None, @@ -68,11 +70,8 @@ def establish_variables(self, x=None, y=None, hue=None, data=None, order = [] # Reduce to just numeric columns for col in data: - try: - data[col].astype(np.float) + if variable_type(data[col]) == "numeric": order.append(col) - except ValueError: - pass plot_data = data[order] group_names = order group_label = data.columns.name @@ -153,7 +152,9 @@ def establish_variables(self, x=None, y=None, hue=None, data=None, raise ValueError(err) # Figure out the plotting orientation - orient = self.infer_orient(x, y, orient) + orient = infer_orient( + x, y, orient, require_numeric=self.require_numeric + ) # Option 2a: # We are plotting a single set of data @@ -321,43 +322,6 @@ def establish_colors(self, color, palette, saturation): self.colors = rgb_colors self.gray = gray - def infer_orient(self, x, y, orient=None): - """Determine how the plot should be oriented based on the data.""" - orient = str(orient) - - def is_categorical(s): - return pd.api.types.is_categorical_dtype(s) - - def is_not_numeric(s): - try: - np.asarray(s, dtype=np.float) - except ValueError: - return True - return False - - no_numeric = "Neither the `x` nor `y` variable appears to be numeric." - - if orient.startswith("v"): - return "v" - elif orient.startswith("h"): - return "h" - elif x is None: - return "v" - elif y is None: - return "h" - elif is_categorical(y): - if is_categorical(x): - raise ValueError(no_numeric) - else: - return "h" - elif is_not_numeric(y): - if is_not_numeric(x): - raise ValueError(no_numeric) - else: - return "h" - else: - return "v" - @property def hue_offsets(self): """A list of center positions for plots when hue nesting is used.""" @@ -1080,6 +1044,7 @@ def plot(self, ax): class _CategoricalScatterPlotter(_CategoricalPlotter): default_palette = "dark" + require_numeric = False @property def point_colors(self): @@ -1456,6 +1421,8 @@ def plot(self, ax, kws): class _CategoricalStatPlotter(_CategoricalPlotter): + require_numeric = True + @property def nested_width(self): """A float with the width of plot elements when hue nesting is used.""" @@ -1819,6 +1786,10 @@ def plot(self, ax): ax.invert_yaxis() +class _CountPlotter(_BarPlotter): + require_numeric = False + + class _LVPlotter(_CategoricalPlotter): def __init__(self, x, y, hue, data, order, hue_order, @@ -2148,9 +2119,9 @@ def plot(self, ax, boxplot_kws): orient=dedent("""\ orient : "v" | "h", optional Orientation of the plot (vertical or horizontal). This is usually - inferred from the dtype of the input variables, but can be used to - specify when the "categorical" variable is a numeric or when plotting - wide-form data.\ + inferred based on the type of the input variables, but it can be used + to resolve ambiguitiy when both `x` and `y` are numeric or when + plotting wide-form data.\ """), color=dedent("""\ color : matplotlib color, optional @@ -3607,14 +3578,14 @@ def countplot( orient = "v" y = x elif x is not None and y is not None: - raise TypeError("Cannot pass values for both `x` and `y`") - else: - raise TypeError("Must pass values for either `x` or `y`") + raise ValueError("Cannot pass values for both `x` and `y`") - plotter = _BarPlotter(x, y, hue, data, order, hue_order, - estimator, ci, n_boot, units, seed, - orient, color, palette, saturation, - errcolor, errwidth, capsize, dodge) + plotter = _CountPlotter( + x, y, hue, data, order, hue_order, + estimator, ci, n_boot, units, seed, + orient, color, palette, saturation, + errcolor, errwidth, capsize, dodge + ) plotter.value_label = "count" @@ -3778,7 +3749,7 @@ def catplot( elif y is None and x is not None: x_, y_, orient = x, x, "v" else: - raise ValueError("Either `x` or `y` must be None for count plots") + raise ValueError("Either `x` or `y` must be None for kind='count'") else: x_, y_ = x, y @@ -3791,7 +3762,19 @@ def catplot( # Determine the order for the whole dataset, which will be used in all # facets to ensure representation of all data in the final plot + plotter_class = { + "box": _BoxPlotter, + "violin": _ViolinPlotter, + "boxen": _LVPlotter, + "lv": _LVPlotter, + "bar": _BarPlotter, + "point": _PointPlotter, + "strip": _StripPlotter, + "swarm": _SwarmPlotter, + "count": _CountPlotter, + }[kind] p = _CategoricalPlotter() + p.require_numeric = plotter_class.require_numeric p.establish_variables(x_, y_, hue, data, orient, order, hue_order) order = p.group_names hue_order = p.hue_names diff --git a/seaborn/conftest.py b/seaborn/conftest.py index 764f530841..c4bb672e6c 100644 --- a/seaborn/conftest.py +++ b/seaborn/conftest.py @@ -143,6 +143,7 @@ def long_df(rng): f=rng.choice([0.2, 0.3], n), )) df["s_cat"] = df["s"].astype("category") + df["s_str"] = df["s"].astype(str) return df diff --git a/seaborn/core.py b/seaborn/core.py index 6ed8aa1b51..67f243eabd 100644 --- a/seaborn/core.py +++ b/seaborn/core.py @@ -1,5 +1,8 @@ import itertools +import warnings from collections.abc import Iterable, Sequence, Mapping +from numbers import Number +from datetime import datetime import numpy as np import pandas as pd @@ -121,8 +124,8 @@ def establish_variables_wideform(self, data=None, **kwargs): wide_data = pd.DataFrame(data, copy=True) # At this point we should reduce the dataframe to numeric cols - # TODO do we want any control over this? - wide_data = wide_data.select_dtypes("number") + numeric_cols = wide_data.apply(variable_type) == "numeric" + wide_data = wide_data.loc[:, numeric_cols] # Now melt the data to long form melt_kws = {"var_name": "columns", "value_name": "values"} @@ -232,6 +235,151 @@ def establish_variables_longform(self, data=None, **kwargs): return plot_data, variables +def variable_type(vector, boolean_type="numeric"): + """Determine whether a vector contains numeric, categorical, or dateime data. + + This function differs from the pandas typing API in two ways: + + - Python sequences or object-typed PyData objects are considered numeric if + all of their entries are numeric. + - String or mixed-type data are considered categorical even if not + explicitly represented as a :class:pandas.api.types.CategoricalDtype`. + + Parameters + ---------- + vector : :func:`pandas.Series`, :func:`numpy.ndarray`, or Python sequence + Input data to test. + binary_type : 'numeric' or 'categorical' + Type to use for vectors containing only 0s and 1s (and NAs). + + Returns + ------- + var_type : 'numeric', 'categorical', or 'datetime' + Name identifying the type of data in the vector. + + """ + # Special-case all-na data, which is always "numeric" + if pd.isna(vector).all(): + return "numeric" + + # Special-case binary/boolean data, allow caller to determine + if np.isin(vector, [0, 1, np.nan]).all(): + return boolean_type + + # Defer to positive pandas tests + if pd.api.types.is_numeric_dtype(vector): + return "numeric" + + if pd.api.types.is_categorical_dtype(vector): + return "categorical" + + if pd.api.types.is_datetime64_dtype(vector): + return "datetime" + + # --- If we get to here, we need to check the entries + + # Check for a collection where everything is a number + + def all_numeric(x): + for x_i in x: + if not isinstance(x_i, Number): + return False + return True + + if all_numeric(vector): + return "numeric" + + # Check for a collection where everything is a datetime + + def all_datetime(x): + for x_i in x: + if not isinstance(x_i, (datetime, np.datetime64)): + return False + return True + + if all_datetime(vector): + return "datetime" + + # Otherwise, our final fallback is to consider things categorical + + return "categorical" + + +def infer_orient(x=None, y=None, orient=None, require_numeric=True): + """Determine how the plot should be oriented based on the data. + + For historical reasons, the convention is to call a plot "horizontally" + or "vertically" oriented based on the axis representing its dependent + variable. Practically, this is used when determining the axis for + numerical aggregation. + + Paramters + --------- + x, y : Vector data or None + Positional data vectors for the plot. + orient : string or None + Specified orientation, which must start with "v" or "h" if not None. + require_numeric : bool + If set, raise when the implied dependent variable is not numeric. + + Returns + ------- + orient : "v" or "h" + + Raises + ------ + ValueError: When `orient` is not None and does not start with "h" or "v" + TypeError: When dependant variable is not numeric, with `require_numeric` + + """ + + x_type = None if x is None else variable_type(x) + y_type = None if y is None else variable_type(y) + + nonnumeric_dv_error = "{} orientation requires numeric `{}` variable." + single_var_warning = "{} orientation ignored with only `{}` specified." + + if x is None: + if str(orient).startswith("h"): + warnings.warn(single_var_warning.format("Horizontal", "y")) + if require_numeric and y_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Vertical", "y")) + return "v" + + elif y is None: + if str(orient).startswith("v"): + warnings.warn(single_var_warning.format("Vertical", "x")) + if require_numeric and x_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Horizontal", "x")) + return "h" + + elif str(orient).startswith("v"): + if require_numeric and y_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Vertical", "y")) + return "v" + + elif str(orient).startswith("h"): + if require_numeric and x_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Horizontal", "x")) + return "h" + + elif orient is not None: + raise ValueError(f"Value for `orient` not understood: {orient}") + + elif x_type != "numeric" and y_type == "numeric": + return "v" + + elif x_type == "numeric" and y_type != "numeric": + return "h" + + elif require_numeric and "numeric" not in (x_type, y_type): + err = "Neither the `x` nor `y` variable appears to be numeric." + raise TypeError(err) + + else: + return "v" + + def unique_dashes(n): """Build an arbitrarily long list of unique dash styles for lines. diff --git a/seaborn/relational.py b/seaborn/relational.py index e134ecb03a..699241e568 100644 --- a/seaborn/relational.py +++ b/seaborn/relational.py @@ -7,7 +7,12 @@ import matplotlib as mpl import matplotlib.pyplot as plt -from .core import (_VectorPlotter, unique_dashes, unique_markers) +from .core import ( + _VectorPlotter, + variable_type, + unique_dashes, + unique_markers, +) from .utils import (categorical_order, get_color_cycle, ci_to_errsize, remove_na, locator_to_legend_entries, ci as ci_func) @@ -212,29 +217,27 @@ def parse_hue(self, data, palette=None, order=None, norm=None): elif isinstance(palette, (dict, list)): var_type = "categorical" - # -- Option 1: categorical color palette - - if var_type == "categorical": + # -- Option 1: quantitative color mapping - cmap = None - limits = None - levels, palette = self.categorical_to_palette( - # List comprehension here is required to - # overcome differences in the way pandas - # externalizes numpy datetime64 - list(data), order, palette - ) + if var_type == "numeric": - # -- Option 2: sequential color palette + data = pd.to_numeric(data) + levels, palette, cmap, norm = self.numeric_to_palette( + data, order, palette, norm + ) + limits = norm.vmin, norm.vmax - elif var_type == "numeric": + # -- Option 2: qualitative color palette - data = pd.to_numeric(data) + else: - levels, palette, cmap, norm = self.numeric_to_palette( - data, order, palette, norm - ) - limits = norm.vmin, norm.vmax + cmap = None + limits = None + levels, palette = self.categorical_to_palette( + # Casting data to list to handle differences in the way + # pandas represents numpy datetime64 data + list(data), order, palette + ) self.hue_levels = levels self.hue_norm = norm @@ -243,11 +246,6 @@ def parse_hue(self, data, palette=None, order=None, norm=None): self.palette = palette self.cmap = cmap - # Update data as it may have changed dtype - # TODO This is messy! We need to rethink the order of operations - # to avoid changing the plot data after we have it. - self.plot_data["hue"] = data - def parse_size(self, data, sizes=None, order=None, norm=None): """Determine the linewidths given data characteristics.""" @@ -406,18 +404,8 @@ def _semantic_type(self, data): """Determine if data should considered numeric or categorical.""" if self.input_format == "wide": return "categorical" - elif isinstance(data, pd.Series) and data.dtype.name == "category": - return "categorical" else: - try: - float_data = data.astype(np.float) - values = np.unique(float_data.dropna()) - # TODO replace with isin when pinned np version >= 1.13 - if np.all(np.in1d(values, np.array([0., 1.]))): - return "categorical" - return "numeric" - except (ValueError, TypeError): - return "categorical" + return variable_type(data, boolean_type="categorical") def label_axes(self, ax): """Set x and y labels with visibility that matches the ticklabels.""" @@ -1590,20 +1578,16 @@ def relplot( row=row, col=col, ) - # Assemble a data object with the plot_data from the original - # plotter and the row/col variables with their external names. - # This is so FacetGrid labels the subplots correctly. - # We can't use just full_data because the hue/size type inference can - # change the data type of variables with object type but numeric behavior. + # Pass the row/col variables to FacetGrid with their original + # names so that the axes titles render correctly grid_kws = {v: full_variables.get(v, None) for v in grid_semantics} - grid_data = full_data[grid_semantics].rename(columns=grid_kws) - plot_data = pd.concat([plot_data, grid_data], axis=1) + full_data = full_data.rename(columns=grid_kws) # Set up the FacetGrid object facet_kws = {} if facet_kws is None else facet_kws.copy() facet_kws.update(grid_kws) g = FacetGrid( - data=plot_data, + data=full_data, col_wrap=col_wrap, row_order=row_order, col_order=col_order, height=height, aspect=aspect, dropna=False, **facet_kws diff --git a/seaborn/tests/test_categorical.py b/seaborn/tests/test_categorical.py index 271db3354d..302a7382ee 100644 --- a/seaborn/tests/test_categorical.py +++ b/seaborn/tests/test_categorical.py @@ -341,30 +341,6 @@ def test_plot_units(self): for group, units in zip(["a", "b", "c"], p.plot_units): npt.assert_array_equal(units, self.u[self.g == group]) - def test_infer_orient(self): - - p = cat._CategoricalPlotter() - - cats = pd.Series(["a", "b", "c"] * 10) - nums = pd.Series(self.rs.randn(30)) - - nt.assert_equal(p.infer_orient(cats, nums), "v") - nt.assert_equal(p.infer_orient(nums, cats), "h") - nt.assert_equal(p.infer_orient(nums, None), "h") - nt.assert_equal(p.infer_orient(None, nums), "v") - nt.assert_equal(p.infer_orient(nums, nums, "vert"), "v") - nt.assert_equal(p.infer_orient(nums, nums, "hori"), "h") - - with nt.assert_raises(ValueError): - p.infer_orient(cats, cats) - - cats = pd.Series([0, 1, 2] * 10, dtype="category") - nt.assert_equal(p.infer_orient(cats, nums), "v") - nt.assert_equal(p.infer_orient(nums, cats), "h") - - with nt.assert_raises(ValueError): - p.infer_orient(cats, cats) - def test_default_palettes(self): p = cat._CategoricalPlotter() @@ -2484,10 +2460,7 @@ def test_plot_elements(self): def test_input_error(self): - with nt.assert_raises(TypeError): - cat.countplot() - - with nt.assert_raises(TypeError): + with nt.assert_raises(ValueError): cat.countplot(x="g", y="h", data=self.df) diff --git a/seaborn/tests/test_core.py b/seaborn/tests/test_core.py index fe2fb37c06..1b3a478a0d 100644 --- a/seaborn/tests/test_core.py +++ b/seaborn/tests/test_core.py @@ -1,10 +1,14 @@ import numpy as np +import pandas as pd import matplotlib as mpl +import pytest from numpy.testing import assert_array_equal from ..core import ( _VectorPlotter, + variable_type, + infer_orient, unique_dashes, unique_markers, ) @@ -63,3 +67,78 @@ def test_unique_markers(self): assert len(set(markers)) == n for m in markers: assert mpl.markers.MarkerStyle(m).is_filled() + + def test_variable_type(self): + + s = pd.Series([1., 2., 3.]) + assert variable_type(s) == "numeric" + assert variable_type(s.astype(int)) == "numeric" + assert variable_type(s.astype(object)) == "numeric" + # assert variable_type(s.to_numpy()) == "numeric" + assert variable_type(s.values) == "numeric" + # assert variable_type(s.to_list()) == "numeric" + assert variable_type(s.tolist()) == "numeric" + + s = pd.Series([1, 2, 3, np.nan], dtype=object) + assert variable_type(s) == "numeric" + + s = pd.Series([np.nan, np.nan]) + # s = pd.Series([pd.NA, pd.NA]) + assert variable_type(s) == "numeric" + + s = pd.Series(["1", "2", "3"]) + assert variable_type(s) == "categorical" + # assert variable_type(s.to_numpy()) == "categorical" + assert variable_type(s.values) == "categorical" + # assert variable_type(s.to_list()) == "categorical" + assert variable_type(s.tolist()) == "categorical" + + s = pd.Series([True, False, False]) + assert variable_type(s) == "numeric" + assert variable_type(s, boolean_type="categorical") == "categorical" + + s = pd.Series([pd.Timestamp(1), pd.Timestamp(2)]) + assert variable_type(s) == "datetime" + assert variable_type(s.astype(object)) == "datetime" + # assert variable_type(s.to_numpy()) == "datetime" + assert variable_type(s.values) == "datetime" + # assert variable_type(s.to_list()) == "datetime" + assert variable_type(s.tolist()) == "datetime" + + def test_infer_orient(self): + + nums = pd.Series(np.arange(6)) + cats = pd.Series(["a", "b"] * 3) + + assert infer_orient(cats, nums) == "v" + assert infer_orient(nums, cats) == "h" + + assert infer_orient(nums, None) == "h" + with pytest.warns(UserWarning, match="Vertical .+ `x`"): + assert infer_orient(nums, None, "v") == "h" + + assert infer_orient(None, nums) == "v" + with pytest.warns(UserWarning, match="Horizontal .+ `y`"): + assert infer_orient(None, nums, "h") == "v" + + infer_orient(cats, None, require_numeric=False) == "h" + with pytest.raises(TypeError, match="Horizontal .+ `x`"): + infer_orient(cats, None) + + infer_orient(cats, None, require_numeric=False) == "v" + with pytest.raises(TypeError, match="Vertical .+ `y`"): + infer_orient(None, cats) + + assert infer_orient(nums, nums, "vert") == "v" + assert infer_orient(nums, nums, "hori") == "h" + + assert infer_orient(cats, cats, "h", require_numeric=False) == "h" + assert infer_orient(cats, cats, "v", require_numeric=False) == "v" + assert infer_orient(cats, cats, require_numeric=False) == "v" + + with pytest.raises(TypeError, match="Vertical .+ `y`"): + infer_orient(cats, cats, "v") + with pytest.raises(TypeError, match="Horizontal .+ `x`"): + infer_orient(cats, cats, "h") + with pytest.raises(TypeError, match="Neither"): + infer_orient(cats, cats) diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py index 61bd99665c..e7dc861abd 100644 --- a/seaborn/tests/test_relational.py +++ b/seaborn/tests/test_relational.py @@ -668,7 +668,7 @@ def test_parse_hue_categorical(self, wide_df, long_df): p.establish_variables(data=long_df, x="x", y="y", hue="t") p.parse_hue(p.plot_data["hue"]) assert p.hue_levels == [pd.Timestamp('2005-02-25')] - assert p.hue_type == "categorical" + assert p.hue_type == "datetime" # Test numeric data with category type p = _RelationalPlotter() diff --git a/seaborn/utils.py b/seaborn/utils.py index 76e72c5620..70390a8b84 100644 --- a/seaborn/utils.py +++ b/seaborn/utils.py @@ -12,6 +12,8 @@ import matplotlib.colors as mplcol import matplotlib.pyplot as plt +from .core import variable_type + __all__ = ["desaturate", "saturate", "set_hls_values", "despine", "get_dataset_names", "get_data_home", "load_dataset"] @@ -542,15 +544,15 @@ def categorical_order(values, order=None): try: order = values.cat.categories except (TypeError, AttributeError): + try: order = values.unique() except AttributeError: order = pd.unique(values) - try: - np.asarray(values).astype(np.float) + + if variable_type(values) == "numeric": order = np.sort(order) - except (ValueError, TypeError): - order = order + order = filter(pd.notnull, order) return list(order) diff --git a/setup.py b/setup.py index 5cdff8cc5c..f955deb648 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,10 @@ PYTHON_REQUIRES = ">=3.6" INSTALL_REQUIRES = [ - 'numpy>=1.13.3', - 'scipy>=1.0.1', - 'pandas>=0.22.0', - 'matplotlib>=2.1.2', + 'numpy>=1.13', + 'scipy>=1.0', + 'pandas>=0.23', + 'matplotlib>=2.1', ] diff --git a/testing/deps_pinned.txt b/testing/deps_pinned.txt index dd1ae815d4..d1a72958e2 100644 --- a/testing/deps_pinned.txt +++ b/testing/deps_pinned.txt @@ -1,5 +1,5 @@ -numpy=1.13.3 -scipy=1.0.1 -pandas=0.22.0 -matplotlib=2.1.2 -statsmodels=0.8.0 +numpy=1.13 +scipy=1.0 +pandas=0.23 +matplotlib=2.1 +statsmodels=0.8