From c0553053dc2af1ca9d778f937776e5509c0011c3 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Sun, 29 Mar 2020 12:58:48 -0600 Subject: [PATCH 01/11] Add line tests for base module, changes to line methods in base module. --- matplotcheck/base.py | 72 +++++++++++--------- matplotcheck/tests/test_base_lines.py | 97 +++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 matplotcheck/tests/test_base_lines.py diff --git a/matplotcheck/base.py b/matplotcheck/base.py index 6bdcc12c..acee8306 100644 --- a/matplotcheck/base.py +++ b/matplotcheck/base.py @@ -1084,33 +1084,36 @@ def assert_line( AssertionError with message `m` or `m2` 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 + # flag_length = False + # 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 + # 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 + # 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']``. @@ -1123,32 +1126,39 @@ 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": + if isinstance(line_types, str): + line_types = [line_types] + + for line_type in line_types: + if line_type == "regression": + try: 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"]' + except ValueError: + 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 - ), + 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..ce41be7c --- /dev/null +++ b/matplotcheck/tests/test_base_lines.py @@ -0,0 +1,97 @@ +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(): + 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): + fig, ax = plt.subplots() + sns.regplot("A", "B", data=pd_df_reg_data, ax=ax) + + return PlotTester(ax) + + +@pytest.fixture +def pt_one2one(): + 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): + 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): + + # 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): + _, 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): + + 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): + pt_reg_data.assert_lines_of_type("regression") + + +def test_line_type_one2one(pt_one2one): + pt_one2one.assert_lines_of_type("onetoone") + + +def test_line_type_reg_one2one(pt_reg_one2one): + pt_reg_one2one.assert_lines_of_type(["regression", "onetoone"]) + + +def test_line_type_reg_fails(pt_one2one): + 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): + with pytest.raises( + AssertionError, match="onetoone line not displayed properly" + ): + pt_reg_data.assert_lines_of_type("onetoone") From bfa569fef74938a03362aa04922243a28cf6b28c Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Mon, 20 Apr 2020 14:18:48 -0600 Subject: [PATCH 02/11] Minor change to assert_xy conversion method. --- matplotcheck/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/matplotcheck/base.py b/matplotcheck/base.py index acee8306..9b991a22 100644 --- a/matplotcheck/base.py +++ b/matplotcheck/base.py @@ -933,13 +933,15 @@ def assert_xydata( that we can use our own message.""" try: np.testing.assert_array_max_ulp( - np.array(xy_data["x"]), np.array(xy_expected[xcol]) + xy_data["x"].to_numpy(dtype=np.float64), + xy_expected[xcol].to_numpy(dtype=np.float64), ) except AssertionError: raise AssertionError(message) try: np.testing.assert_array_max_ulp( - np.array(xy_data["y"]), np.array(xy_expected[ycol]) + xy_data["y"].to_numpy(dtype=np.float64), + xy_expected[ycol].to_numpy(dtype=np.float64), ) except AssertionError: raise AssertionError(message) From baee57e12512c490e9ba19c57fddf037bcd4c65a Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Mon, 20 Apr 2020 14:21:14 -0600 Subject: [PATCH 03/11] Revert "Minor change to assert_xy conversion method." This reverts commit bfa569fef74938a03362aa04922243a28cf6b28c. --- matplotcheck/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/matplotcheck/base.py b/matplotcheck/base.py index 9b991a22..acee8306 100644 --- a/matplotcheck/base.py +++ b/matplotcheck/base.py @@ -933,15 +933,13 @@ def assert_xydata( that we can use our own message.""" try: np.testing.assert_array_max_ulp( - xy_data["x"].to_numpy(dtype=np.float64), - xy_expected[xcol].to_numpy(dtype=np.float64), + np.array(xy_data["x"]), np.array(xy_expected[xcol]) ) except AssertionError: raise AssertionError(message) try: np.testing.assert_array_max_ulp( - xy_data["y"].to_numpy(dtype=np.float64), - xy_expected[ycol].to_numpy(dtype=np.float64), + np.array(xy_data["y"]), np.array(xy_expected[ycol]) ) except AssertionError: raise AssertionError(message) From 9ca93cf5f9b771b88df85e674c0bd4968ee6bc94 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Sun, 26 Apr 2020 21:45:25 -0600 Subject: [PATCH 04/11] Improved handline of lines without xy-data (Issue #238) --- matplotcheck/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/matplotcheck/base.py b/matplotcheck/base.py index acee8306..53122951 100644 --- a/matplotcheck/base.py +++ b/matplotcheck/base.py @@ -1131,12 +1131,14 @@ def assert_lines_of_type(self, line_types): for line_type in line_types: if line_type == "regression": - try: - xy = self.get_xy(points_only=True) - slope_exp, intercept_exp, _, _, _ = stats.linregress( - xy.x, xy.y - ) - except ValueError: + xy = self.get_xy(points_only=True) + slope_exp, intercept_exp, _, _, _ = stats.linregress( + xy.x, xy.y + ) + + # 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" ) From 92ae52151a108764dce73a09a27471bc640374f6 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Sun, 26 Apr 2020 22:00:35 -0600 Subject: [PATCH 05/11] Added docstrings to line tests. --- matplotcheck/tests/test_base_lines.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/matplotcheck/tests/test_base_lines.py b/matplotcheck/tests/test_base_lines.py index ce41be7c..9318d9e9 100644 --- a/matplotcheck/tests/test_base_lines.py +++ b/matplotcheck/tests/test_base_lines.py @@ -10,6 +10,8 @@ @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], @@ -20,6 +22,7 @@ def pd_df_reg_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) @@ -28,6 +31,7 @@ def pt_reg_data(pd_df_reg_data): @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") @@ -36,6 +40,8 @@ def pt_one2one(): @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") @@ -44,7 +50,8 @@ def pt_reg_one2one(pd_df_reg_data): 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 @@ -54,6 +61,8 @@ def test_reg_plot(pd_df_reg_data, pt_reg_data): 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 ) @@ -62,7 +71,8 @@ def test_reg_plot_slope_fails(pd_df_reg_data, pt_reg_data): 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 ) @@ -72,18 +82,26 @@ def test_reg_plot_intercept_fails(pd_df_reg_data, pt_reg_data): 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" ): @@ -91,6 +109,8 @@ def test_line_type_reg_fails(pt_one2one): 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" ): From 5e3372858adad0ff3c3e248d01339475b86582df Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Sun, 26 Apr 2020 22:20:23 -0600 Subject: [PATCH 06/11] Added Seaborn to dev requirements --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 4b900499..85229890 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -11,3 +11,4 @@ setuptools==46.1.3 pre-commit==1.20.0 pip==19.0.3 descartes==1.1.0 +seaborn==0.10.0 From 76f26240835a54eada34bc995481febf4d0c695d Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Sun, 26 Apr 2020 22:23:21 -0600 Subject: [PATCH 07/11] Undo changes regarding x-limits problems (Issue #235) --- matplotcheck/base.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/matplotcheck/base.py b/matplotcheck/base.py index 53122951..ecc4d022 100644 --- a/matplotcheck/base.py +++ b/matplotcheck/base.py @@ -1085,9 +1085,9 @@ def assert_line( with message `m` or `m2` if no line exists that covers the dataset """ flag_exist = False - # flag_length = False - # xy = self.get_xy(points_only=True) - # min_val, max_val = min(xy["x"]), max(xy["x"]) + flag_length = False + xy = self.get_xy(points_only=True) + min_val, max_val = min(xy["x"]), max(xy["x"]) for l in self.ax.lines: # Here we will get the verticies for the line and reformat them in @@ -1100,13 +1100,13 @@ def assert_line( 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 + 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 + assert flag_length, message_data def assert_lines_of_type(self, line_types): """Asserts each line of type in `line_types` exist on `ax` @@ -1132,16 +1132,15 @@ def assert_lines_of_type(self, 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 - ) - # 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" ) + slope_exp, intercept_exp, _, _, _ = stats.linregress( + xy.x, xy.y + ) elif line_type == "onetoone": slope_exp, intercept_exp = 1, 0 else: From 5352acc98daecdeb131369c977e87108bc46e0f0 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Mon, 4 May 2020 02:54:16 -0600 Subject: [PATCH 08/11] Made checking line coverage optional for base.assert_line() --- CHANGELOG.rst | 3 +++ matplotcheck/base.py | 40 +++++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 15 deletions(-) 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/matplotcheck/base.py b/matplotcheck/base.py index 1fe75222..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,23 +1074,29 @@ 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 = False - flag_length = False - xy = self.get_xy(points_only=True) - min_val, max_val = min(xy["x"]), max(xy["x"]) + + 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: # Here we will get the verticies for the line and reformat them in @@ -1104,13 +1109,18 @@ def assert_line( 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` From 5b51d0e586b794b310cc9248f2c87f47f5054268 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Mon, 4 May 2020 03:04:21 -0600 Subject: [PATCH 09/11] Fix double requirement in dev-requirements.txt --- dev-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 8b509f28..a385b144 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,6 +10,5 @@ setuptools==46.1.3 pre-commit==1.20.0 pip==19.0.3 descartes==1.1.0 -pillow==7.1.1 seaborn==0.10.0 pillow==7.1.2 From 7dc3c9ad5e274c886d2a9ff1b03fcf2ae64eb237 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Mon, 4 May 2020 03:13:00 -0600 Subject: [PATCH 10/11] Change seaborn version --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index a385b144..c9f7be45 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,5 +10,5 @@ setuptools==46.1.3 pre-commit==1.20.0 pip==19.0.3 descartes==1.1.0 -seaborn==0.10.0 +seaborn==0.9.1 pillow==7.1.2 From ae7c000781f6ca928d022dbe0c83d9fe57dd4ac4 Mon Sep 17 00:00:00 2001 From: "Ryan (Marty) LaRocque" Date: Mon, 4 May 2020 03:24:50 -0600 Subject: [PATCH 11/11] Seaborn requirement version change --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index c9f7be45..8c89ea2e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,5 +10,5 @@ setuptools==46.1.3 pre-commit==1.20.0 pip==19.0.3 descartes==1.1.0 -seaborn==0.9.1 +seaborn>=0.9.1 pillow==7.1.2