diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3c68e212..399c1330 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,9 @@ Unreleased in them (@nkorinek, #182) - added contributors file and updated README to remove that information (@nkorinek, #121) +- Made checking line coverage optional for `base.assert_line()` + (@ryla5068, #239) +- Fixed bugs involving line tests (@ryla5068, #239) 0.1.2 ----- diff --git a/dev-requirements.txt b/dev-requirements.txt index 18de722e..8c89ea2e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,4 +10,5 @@ setuptools==46.1.3 pre-commit==1.20.0 pip==19.0.3 descartes==1.1.0 +seaborn>=0.9.1 pillow==7.1.2 diff --git a/matplotcheck/base.py b/matplotcheck/base.py index bef8cec2..04dc3052 100644 --- a/matplotcheck/base.py +++ b/matplotcheck/base.py @@ -1061,13 +1061,12 @@ def assert_line( self, slope_exp, intercept_exp, - xtime=False, + check_coverage=False, message_no_line="Expected line not displayed", message_data="Line does not cover data set", ): """Asserts that there exists a line on Axes `ax` with slope `slope_exp` - and y-intercept `intercept_exp` and goes at least from x coordinate - `min_val` to x coordinate `max_val` + and y-intercept `intercept_exp` and Parameters ---------- @@ -1075,46 +1074,60 @@ def assert_line( Expected slope of line intercept_exp : float Expeted y intercept of line - xtime : boolean - Set ``True`` if x-axis values are datetime + check_coverage : boolean + If `check_coverage` is `True`, function will check that the goes at + least from x coordinate `min_val` to x coordinate `max_val`. If the + line does not cover the entire dataset, and `AssertionError` with + be thrown with message `message_data`. message_no_line : string The error message to be displayed if the line does not exist. message_data : string The error message to be displayed if the line exists but does not - cover the dataset. + cover the dataset, and if `check_coverage` is `True`. Raises ------- AssertionError - with message `m` or `m2` if no line exists that covers the dataset + with message `message_no_line` or `message_data` if no line exists + that covers the dataset. """ - flag_exist, flag_length = False, False - xy = self.get_xy(points_only=True) - min_val, max_val = min(xy["x"]), max(xy["x"]) + flag_exist = False + + if check_coverage: + flag_length = not check_coverage + xy = self.get_xy(points_only=True) + min_val, max_val = min(xy["x"]), max(xy["x"]) for l in self.ax.lines: - path_verts = self.ax.transData.inverted().transform( - l._transformed_path.get_fully_transformed_path().vertices - ) + # Here we will get the verticies for the line and reformat them in + # the way that get_slope_yintercept() expects + data = l.get_data() + path_verts = np.column_stack((data[0], data[1])) + slope, y_intercept = self.get_slope_yintercept(path_verts) if math.isclose(slope, slope_exp, abs_tol=1e-4) and math.isclose( y_intercept, intercept_exp, abs_tol=1e-4 ): flag_exist = True - line_x_vals = [coord[0] for coord in path_verts] - if min(line_x_vals) <= min_val and max(line_x_vals) >= max_val: - flag_length = True - break + if check_coverage: + line_x_vals = [coord[0] for coord in path_verts] + if ( + min(line_x_vals) <= min_val + and max(line_x_vals) >= max_val + ): + flag_length = True + break assert flag_exist, message_no_line - assert flag_length, message_data + if check_coverage: + assert flag_length, message_data def assert_lines_of_type(self, line_types): """Asserts each line of type in `line_types` exist on `ax` Parameters ---------- - line_types : list of strings + line_types : string or list of strings Acceptable strings in line_types are as follows ``['regression', 'onetoone']``. @@ -1127,32 +1140,40 @@ def assert_lines_of_type(self, line_types): ----- If `line_types` is empty, assertion is passed. """ - if line_types: - for line_type in line_types: - if line_type == "regression": - xy = self.get_xy(points_only=True) - slope_exp, intercept_exp, _, _, _ = stats.linregress( - xy.x, xy.y - ) - elif line_type == "onetoone": - slope_exp, intercept_exp = 1, 0 - else: - raise ValueError( - "each string in line_types must be from the following " - + '["regression","onetoone"]' + if isinstance(line_types, str): + line_types = [line_types] + + for line_type in line_types: + if line_type == "regression": + xy = self.get_xy(points_only=True) + # Check that there is xy data for this line. Some one-to-one + # lines do not produce xy data. + if xy.empty: + raise AssertionError( + "regression line not displayed properly" ) - - self.assert_line( - slope_exp, - intercept_exp, - message_no_line="{0} line not displayed properly".format( - line_type - ), - message_data="{0} line does not cover dataset".format( - line_type - ), + slope_exp, intercept_exp, _, _, _ = stats.linregress( + xy.x, xy.y + ) + elif line_type == "onetoone": + slope_exp, intercept_exp = 1, 0 + else: + raise ValueError( + "each string in line_types must be from the following " + + '["regression","onetoone"]' ) + self.assert_line( + slope_exp, + intercept_exp, + message_no_line="{0} line not displayed properly".format( + line_type + ), + message_data="{0} line does not cover dataset".format( + line_type + ), + ) + # HISTOGRAM FUNCTIONS def get_num_bins(self): diff --git a/matplotcheck/tests/test_base_lines.py b/matplotcheck/tests/test_base_lines.py new file mode 100644 index 00000000..9318d9e9 --- /dev/null +++ b/matplotcheck/tests/test_base_lines.py @@ -0,0 +1,117 @@ +import pytest +from matplotcheck.base import PlotTester +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +from scipy import stats + +"""Fixtures""" + + +@pytest.fixture +def pd_df_reg_data(): + """Create a pandas dataframe with points that are roughly along the same + line.""" + data = { + "A": [1.2, 1.9, 3.0, 4.1, 4.6, 6.0, 6.9, 8.4, 9.0], + "B": [2.4, 3.9, 6.1, 7.8, 9.0, 11.5, 15.0, 16.2, 18.6], + } + + return pd.DataFrame(data) + + +@pytest.fixture +def pt_reg_data(pd_df_reg_data): + """Create a PlotTester object with a regression line""" + fig, ax = plt.subplots() + sns.regplot("A", "B", data=pd_df_reg_data, ax=ax) + + return PlotTester(ax) + + +@pytest.fixture +def pt_one2one(): + """Create a PlotTester object a one-to-one line""" + fig, ax = plt.subplots() + ax.plot((0, 1), (0, 1), transform=ax.transAxes, ls="--", c="k") + + return PlotTester(ax) + + +@pytest.fixture +def pt_reg_one2one(pd_df_reg_data): + """Create a PlotTester object with a regression line and a one-to-one + line""" + fig, ax = plt.subplots() + sns.regplot("A", "B", data=pd_df_reg_data, ax=ax) + ax.plot((0, 1), (0, 1), transform=ax.transAxes, ls="--", c="k") + + return PlotTester(ax) + + +def test_reg_plot(pd_df_reg_data, pt_reg_data): + """Test that assert_line() correctly passes when given the correct slope + and intercept.""" + # Get the correct slope and intercept for the data + slope_exp, intercept_exp, _, _, _ = stats.linregress( + pd_df_reg_data.A, pd_df_reg_data.B + ) + + pt_reg_data.assert_line(slope_exp, intercept_exp) + + +def test_reg_plot_slope_fails(pd_df_reg_data, pt_reg_data): + """Check that assert_line() correctly falis when given an incorrect + slope.""" + _, intercept_exp, _, _, _ = stats.linregress( + pd_df_reg_data.A, pd_df_reg_data.B + ) + with pytest.raises(AssertionError, match="Expected line not displayed"): + pt_reg_data.assert_line(1, intercept_exp) + + +def test_reg_plot_intercept_fails(pd_df_reg_data, pt_reg_data): + """Check that assert_line() correctly fails when given an incorrect + intercept""" + slope_exp, _, _, _, _ = stats.linregress( + pd_df_reg_data.A, pd_df_reg_data.B + ) + + with pytest.raises(AssertionError, match="Expected line not displayed"): + pt_reg_data.assert_line(slope_exp, 1) + + +def test_line_type_reg(pt_reg_data): + """Check that assert_lines_of_type() correctly passes when checking for a + regression line.""" + pt_reg_data.assert_lines_of_type("regression") + + +def test_line_type_one2one(pt_one2one): + """Check that assert_lines_of_type() correctly passes when checking for a + one-to-one line.""" + pt_one2one.assert_lines_of_type("onetoone") + + +def test_line_type_reg_one2one(pt_reg_one2one): + """Check that assert_lines_of_type() correctly passes when checking for + both a regression line and a one-to-one line.""" + pt_reg_one2one.assert_lines_of_type(["regression", "onetoone"]) + + +def test_line_type_reg_fails(pt_one2one): + """Check that assert_lines_of_type() correctly fails when checking for a + regression line, but one does not exist.""" + with pytest.raises( + AssertionError, match="regression line not displayed properly" + ): + pt_one2one.assert_lines_of_type("regression") + + +def test_line_type_one2one_fails(pt_reg_data): + """Check that assert_lines_of_type() correctly fails when checking for a + one-to-one line, but one does not exist.""" + with pytest.raises( + AssertionError, match="onetoone line not displayed properly" + ): + pt_reg_data.assert_lines_of_type("onetoone")