Repository: great-expectations/great_expectations

Issue:
Use cleaner solution for non-truncating division in python 2
Prefer `from __future__ import division` to `1.*x/y`


Discussion:
Done inline during improve_partitions push.

Resolving patch:
diff --git a/great_expectations/expectations/metrics/column_pair_map_metrics/column_pair_values_greater.py b/great_expectations/expectations/metrics/column_pair_map_metrics/column_pair_values_greater.py
--- a/great_expectations/expectations/metrics/column_pair_map_metrics/column_pair_values_greater.py
+++ b/great_expectations/expectations/metrics/column_pair_map_metrics/column_pair_values_greater.py
@@ -1,20 +1,13 @@
-from typing import Any, Dict, Tuple
-
 from dateutil.parser import parse
 
 from great_expectations.execution_engine import PandasExecutionEngine
-from great_expectations.execution_engine.execution_engine import (
-    MetricDomainTypes,
-    MetricPartialFunctionTypes,
-)
 from great_expectations.expectations.metrics.map_metric_provider import (
-    MapMetricProvider,
+    ColumnPairMapMetricProvider,
+    column_pair_condition_partial,
 )
-from great_expectations.expectations.metrics.metric_provider import metric_partial
-from great_expectations.expectations.metrics.util import filter_pair_metric_nulls
 
 
-class ColumnPairValuesAGreaterThanB(MapMetricProvider):
+class ColumnPairValuesAGreaterThanB(ColumnPairMapMetricProvider):
     condition_metric_name = "column_pair_values.a_greater_than_b"
     condition_value_keys = (
         "ignore_row_if",
@@ -22,55 +15,28 @@ class ColumnPairValuesAGreaterThanB(MapMetricProvider):
         "parse_strings_as_datetimes",
         "allow_cross_type_comparisons",
     )
-    domain_keys = ("batch_id", "table", "column_A", "column_B")
-
-    @metric_partial(
-        engine=PandasExecutionEngine,
-        partial_fn_type=MetricPartialFunctionTypes.MAP_CONDITION_SERIES,
-        domain_type=MetricDomainTypes.COLUMN_PAIR,
-    )
-    def _pandas(
-        cls,
-        execution_engine: "PandasExecutionEngine",
-        metric_domain_kwargs: Dict,
-        metric_value_kwargs: Dict,
-        metrics: Dict[Tuple, Any],
-        runtime_configuration: Dict,
-    ):
-
-        ignore_row_if = metric_value_kwargs.get("ignore_row_if")
-        if not ignore_row_if:
-            ignore_row_if = "both_values_are_missing"
-        or_equal = metric_value_kwargs.get("or_equal")
-        parse_strings_as_datetimes = metric_value_kwargs.get(
-            "parse_strings_as_datetimes"
-        )
-        allow_cross_type_comparisons = metric_value_kwargs.get(
-            "allow_cross_type_comparisons"
-        )
-
-        df, compute_domain, accessor_domain = execution_engine.get_compute_domain(
-            metric_domain_kwargs, MetricDomainTypes.COLUMN_PAIR
-        )
-
-        column_A, column_B = filter_pair_metric_nulls(
-            df[metric_domain_kwargs["column_A"]],
-            df[metric_domain_kwargs["column_B"]],
-            ignore_row_if=ignore_row_if,
-        )
+    condition_domain_keys = ("batch_id", "table", "column_A", "column_B")
 
+    # TODO: <Alex>ALEX -- temporarily only a Pandas implementation is provided (others to follow).</Alex>
+    # noinspection PyPep8Naming
+    @column_pair_condition_partial(engine=PandasExecutionEngine)
+    def _pandas(cls, column_A, column_B, **kwargs):
+        allow_cross_type_comparisons = kwargs.get("allow_cross_type_comparisons")
         if allow_cross_type_comparisons:
             raise NotImplementedError
 
+        parse_strings_as_datetimes = kwargs.get("parse_strings_as_datetimes")
         if parse_strings_as_datetimes:
+            # noinspection PyPep8Naming
             temp_column_A = column_A.map(parse)
+            # noinspection PyPep8Naming
             temp_column_B = column_B.map(parse)
-
         else:
             temp_column_A = column_A
             temp_column_B = column_B
 
+        or_equal = kwargs.get("or_equal")
         if or_equal:
-            return temp_column_A >= temp_column_B, compute_domain, accessor_domain
+            return temp_column_A >= temp_column_B
         else:
-            return temp_column_A > temp_column_B, compute_domain, accessor_domain
+            return temp_column_A > temp_column_B
diff --git a/great_expectations/expectations/metrics/map_metric_provider.py b/great_expectations/expectations/metrics/map_metric_provider.py
--- a/great_expectations/expectations/metrics/map_metric_provider.py
+++ b/great_expectations/expectations/metrics/map_metric_provider.py
@@ -502,6 +502,178 @@ def inner_func(
         raise ValueError("Unsupported engine for column_condition_partial")
 
 
+def column_pair_function_partial(
+    engine: Type[ExecutionEngine], partial_fn_type: str = None, **kwargs
+):
+    """Provides engine-specific support for authoring a metric_fn with a simplified signature.
+
+    A metric function that is decorated as a column_pair_function_partial will be called with the engine-specific
+    column_list type and any value_kwargs associated with the Metric for which the provider function is being declared.
+
+    Args:
+        engine:
+        partial_fn_type:
+        **kwargs:
+
+    Returns:
+        An annotated metric_function which will be called with a simplified signature.
+
+    """
+    # TODO: <Alex>ALEX -- temporarily only a Pandas implementation is provided (others to follow).</Alex>
+    domain_type = MetricDomainTypes.COLUMN_PAIR
+    if issubclass(engine, PandasExecutionEngine):
+        if partial_fn_type is None:
+            partial_fn_type = MetricPartialFunctionTypes.MAP_SERIES
+        partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
+        if partial_fn_type != MetricPartialFunctionTypes.MAP_SERIES:
+            raise ValueError(
+                "PandasExecutionEngine only supports map_series for column_pair_function_partial partial_fn_type"
+            )
+
+        def wrapper(metric_fn: Callable):
+            @metric_partial(
+                engine=engine,
+                partial_fn_type=partial_fn_type,
+                domain_type=domain_type,
+                **kwargs,
+            )
+            @wraps(metric_fn)
+            def inner_func(
+                cls,
+                execution_engine: PandasExecutionEngine,
+                metric_domain_kwargs: Dict,
+                metric_value_kwargs: Dict,
+                metrics: Dict[str, Any],
+                runtime_configuration: Dict,
+            ):
+                (
+                    df,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                ) = execution_engine.get_compute_domain(
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
+                )
+
+                # noinspection PyPep8Naming
+                column_A_name = accessor_domain_kwargs["column_A"]
+                # noinspection PyPep8Naming
+                column_B_name = accessor_domain_kwargs["column_B"]
+
+                column_list = [column_A_name, column_B_name]
+
+                for column_name in column_list:
+                    if column_name not in metrics["table.columns"]:
+                        raise ge_exceptions.ExecutionEngineError(
+                            message=f'Error: The column "{column_name}" in BatchData does not exist.'
+                        )
+
+                values = metric_fn(
+                    cls,
+                    df[column_A_name],
+                    df[column_B_name],
+                    **metric_value_kwargs,
+                    _metrics=metrics,
+                )
+                return values, compute_domain_kwargs, accessor_domain_kwargs
+
+            return inner_func
+
+        return wrapper
+
+    else:
+        raise ValueError("Unsupported engine for column_pair_function_partial")
+
+
+def column_pair_condition_partial(
+    engine: Type[ExecutionEngine],
+    partial_fn_type: Optional[Union[str, MetricPartialFunctionTypes]] = None,
+    **kwargs,
+):
+    """Provides engine-specific support for authoring a metric_fn with a simplified signature. A
+    column_pair_condition_partial must provide a map function that evaluates to a boolean value; it will be used to
+    provide supplemental metrics, such as the unexpected_value count, unexpected_values, and unexpected_rows.
+
+    A metric function that is decorated as a column_pair_condition_partial will be called with the engine-specific
+    column_list type and any value_kwargs associated with the Metric for which the provider function is being declared.
+
+    Args:
+        engine:
+        partial_fn_type:
+        **kwargs:
+
+    Returns:
+        An annotated metric_function which will be called with a simplified signature.
+
+    """
+    # TODO: <Alex>ALEX -- temporarily only a Pandas implementation is provided (others to follow).</Alex>
+    domain_type = MetricDomainTypes.COLUMN_PAIR
+    if issubclass(engine, PandasExecutionEngine):
+        if partial_fn_type is None:
+            partial_fn_type = MetricPartialFunctionTypes.MAP_CONDITION_SERIES
+        partial_fn_type = MetricPartialFunctionTypes(partial_fn_type)
+        if partial_fn_type not in [MetricPartialFunctionTypes.MAP_CONDITION_SERIES]:
+            raise ValueError(
+                "PandasExecutionEngine only supports map_condition_series for column_pair_condition_partial partial_fn_type"
+            )
+
+        def wrapper(metric_fn: Callable):
+            @metric_partial(
+                engine=engine,
+                partial_fn_type=partial_fn_type,
+                domain_type=domain_type,
+                **kwargs,
+            )
+            @wraps(metric_fn)
+            def inner_func(
+                cls,
+                execution_engine: PandasExecutionEngine,
+                metric_domain_kwargs: Dict,
+                metric_value_kwargs: Dict,
+                metrics: Dict[str, Any],
+                runtime_configuration: Dict,
+            ):
+                (
+                    df,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                ) = execution_engine.get_compute_domain(
+                    domain_kwargs=metric_domain_kwargs, domain_type=domain_type
+                )
+
+                # noinspection PyPep8Naming
+                column_A_name = accessor_domain_kwargs["column_A"]
+                # noinspection PyPep8Naming
+                column_B_name = accessor_domain_kwargs["column_B"]
+
+                column_list = [column_A_name, column_B_name]
+
+                for column_name in column_list:
+                    if column_name not in metrics["table.columns"]:
+                        raise ge_exceptions.ExecutionEngineError(
+                            message=f'Error: The column "{column_name}" in BatchData does not exist.'
+                        )
+
+                meets_expectation_series = metric_fn(
+                    cls,
+                    df[column_A_name],
+                    df[column_B_name],
+                    **metric_value_kwargs,
+                    _metrics=metrics,
+                )
+                return (
+                    ~meets_expectation_series,
+                    compute_domain_kwargs,
+                    accessor_domain_kwargs,
+                )
+
+            return inner_func
+
+        return wrapper
+
+    else:
+        raise ValueError("Unsupported engine for column_pair_condition_partial")
+
+
 def multicolumn_function_partial(
     engine: Type[ExecutionEngine], partial_fn_type: str = None, **kwargs
 ):
@@ -632,7 +804,7 @@ def inner_func(
         return wrapper
 
     else:
-        raise ValueError("Unsupported engine for column_function_partial")
+        raise ValueError("Unsupported engine for multicolumn_function_partial")
 
 
 def multicolumn_condition_partial(
@@ -782,7 +954,7 @@ def inner_func(
         return wrapper
 
     else:
-        raise ValueError("Unsupported engine for column_condition_partial")
+        raise ValueError("Unsupported engine for multicolumn_condition_partial")
 
 
 def _pandas_map_condition_unexpected_count(
@@ -853,6 +1025,109 @@ def _pandas_column_map_condition_values(
         return list(domain_values[: result_format["partial_unexpected_count"]])
 
 
+def _pandas_column_pair_map_condition_values(
+    cls,
+    execution_engine: PandasExecutionEngine,
+    metric_domain_kwargs: Dict,
+    metric_value_kwargs: Dict,
+    metrics: Dict[str, Any],
+    **kwargs,
+):
+    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    (
+        boolean_mapped_unexpected_values,
+        compute_domain_kwargs,
+        accessor_domain_kwargs,
+    ) = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
+
+    if not ("column_A" in domain_kwargs and "column_B" in domain_kwargs):
+        raise ValueError(
+            """No "column_A" and "column_B" found in provided metric_domain_kwargs, but it is required for a column pair map metric
+(_pandas_column_pair_map_condition_values).
+"""
+        )
+
+    # noinspection PyPep8Naming
+    column_A_name = accessor_domain_kwargs["column_A"]
+    # noinspection PyPep8Naming
+    column_B_name = accessor_domain_kwargs["column_B"]
+
+    column_list = [column_A_name, column_B_name]
+
+    for column_name in column_list:
+        if column_name not in metrics["table.columns"]:
+            raise ge_exceptions.ExecutionEngineError(
+                message=f'Error: The column "{column_name}" in BatchData does not exist.'
+            )
+
+    domain_values = df[column_list]
+
+    domain_values = domain_values[boolean_mapped_unexpected_values == True]
+
+    result_format = metric_value_kwargs["result_format"]
+
+    unexpected_list = [
+        value_pair
+        for value_pair in zip(
+            domain_values[column_A_name].values, domain_values[column_B_name].values
+        )
+    ]
+    if result_format["result_format"] == "COMPLETE":
+        return unexpected_list
+    else:
+        return unexpected_list[: result_format["partial_unexpected_count"]]
+
+
+def _pandas_column_pair_map_condition_filtered_row_count(
+    cls,
+    execution_engine: PandasExecutionEngine,
+    metric_domain_kwargs: Dict,
+    metric_value_kwargs: Dict,
+    metrics: Dict[str, Any],
+    **kwargs,
+):
+    """Return values from the specified domain that match the map-style metric in the metrics dictionary."""
+    _, compute_domain_kwargs, accessor_domain_kwargs = metrics["unexpected_condition"]
+    """
+    In order to invoke the "ignore_row_if" filtering logic, "execution_engine.get_domain_records()" must be supplied
+    with all of the available "domain_kwargs" keys.
+    """
+    domain_kwargs = dict(**compute_domain_kwargs, **accessor_domain_kwargs)
+    df = execution_engine.get_domain_records(
+        domain_kwargs=domain_kwargs,
+    )
+
+    if not ("column_A" in domain_kwargs and "column_B" in domain_kwargs):
+        raise ValueError(
+            """No "column_A" and "column_B" found in provided metric_domain_kwargs, but it is required for a column pair map metric
+(_pandas_column_pair_map_condition_filtered_row_count).
+"""
+        )
+
+    # noinspection PyPep8Naming
+    column_A_name = accessor_domain_kwargs["column_A"]
+    # noinspection PyPep8Naming
+    column_B_name = accessor_domain_kwargs["column_B"]
+
+    column_list = [column_A_name, column_B_name]
+
+    for column_name in column_list:
+        if column_name not in metrics["table.columns"]:
+            raise ge_exceptions.ExecutionEngineError(
+                message=f'Error: The column "{column_name}" in BatchData does not exist.'
+            )
+
+    return df.shape[0]
+
+
 def _pandas_multicolumn_map_condition_values(
     cls,
     execution_engine: PandasExecutionEngine,
@@ -1787,6 +2062,25 @@ def _register_metric_functions(cls):
                             metric_provider=_pandas_column_map_condition_value_counts,
                             metric_fn_type=MetricFunctionTypes.VALUE,
                         )
+                    elif domain_type == MetricDomainTypes.COLUMN_PAIR:
+                        register_metric(
+                            metric_name=metric_name + ".unexpected_values",
+                            metric_domain_keys=metric_domain_keys,
+                            metric_value_keys=(*metric_value_keys, "result_format"),
+                            execution_engine=engine,
+                            metric_class=cls,
+                            metric_provider=_pandas_column_pair_map_condition_values,
+                            metric_fn_type=MetricFunctionTypes.VALUE,
+                        )
+                        register_metric(
+                            metric_name=metric_name + ".filtered_row_count",
+                            metric_domain_keys=metric_domain_keys,
+                            metric_value_keys=(*metric_value_keys, "result_format"),
+                            execution_engine=engine,
+                            metric_class=cls,
+                            metric_provider=_pandas_column_pair_map_condition_filtered_row_count,
+                            metric_fn_type=MetricFunctionTypes.VALUE,
+                        )
                     elif domain_type == MetricDomainTypes.MULTICOLUMN:
                         register_metric(
                             metric_name=metric_name + ".unexpected_values",
@@ -2109,6 +2403,56 @@ def _get_evaluation_dependencies(
         return dependencies
 
 
+class ColumnPairMapMetricProvider(MapMetricProvider):
+    condition_domain_keys = (
+        "batch_id",
+        "table",
+        "column_A",
+        "column_B",
+        "row_condition",
+        "condition_parser",
+        "ignore_row_if",
+    )
+    function_domain_keys = (
+        "batch_id",
+        "table",
+        "column_A",
+        "column_B",
+        "row_condition",
+        "condition_parser",
+        "ignore_row_if",
+    )
+    condition_value_keys = tuple()
+    function_value_keys = tuple()
+
+    @classmethod
+    def _get_evaluation_dependencies(
+        cls,
+        metric: MetricConfiguration,
+        configuration: Optional[ExpectationConfiguration] = None,
+        execution_engine: Optional[ExecutionEngine] = None,
+        runtime_configuration: Optional[dict] = None,
+    ):
+        dependencies: dict = super()._get_evaluation_dependencies(
+            metric=metric,
+            configuration=configuration,
+            execution_engine=execution_engine,
+            runtime_configuration=runtime_configuration,
+        )
+        table_domain_kwargs: dict = {
+            k: v
+            for k, v in metric.metric_domain_kwargs.items()
+            if k not in ["column_A", "column_B", "ignore_row_if"]
+        }
+        dependencies["table.columns"] = MetricConfiguration(
+            metric_name="table.columns",
+            metric_domain_kwargs=table_domain_kwargs,
+            metric_value_kwargs=None,
+            metric_dependencies=None,
+        )
+        return dependencies
+
+
 class MulticolumnMapMetricProvider(MapMetricProvider):
     condition_domain_keys = (
         "batch_id",


Test patch:
diff --git a/tests/expectations/metrics/test_core.py b/tests/expectations/metrics/test_core.py
--- a/tests/expectations/metrics/test_core.py
+++ b/tests/expectations/metrics/test_core.py
@@ -1110,19 +1110,39 @@ def test_column_pairs_equal_metric_pd():
 def test_column_pairs_greater_metric_pd():
     df = pd.DataFrame({"a": [2, 3, 4, None, 3, None], "b": [1, 2, 3, None, 3, 5]})
     engine = PandasExecutionEngine(batch_data_dict={"my_id": df})
-    desired_metric = MetricConfiguration(
+
+    metrics: dict = {}
+
+    table_columns_metric: MetricConfiguration
+    results: dict
+
+    table_columns_metric, results = get_table_columns_metric(engine=engine)
+    metrics.update(results)
+
+    condition_metric = MetricConfiguration(
         metric_name="column_pair_values.a_greater_than_b.condition",
-        metric_domain_kwargs={"column_A": "a", "column_B": "b"},
+        metric_domain_kwargs={
+            "column_A": "a",
+            "column_B": "b",
+            "ignore_row_if": "either_value_is_missing",
+        },
         metric_value_kwargs={
             "or_equal": True,
-            "ignore_row_if": "either_value_is_missing",
+        },
+        metric_dependencies={
+            "table.columns": table_columns_metric,
         },
     )
-    results = engine.resolve_metrics(metrics_to_resolve=(desired_metric,))
+    results = engine.resolve_metrics(
+        metrics_to_resolve=(condition_metric,),
+        metrics=metrics,
+    )
+    metrics.update(results)
+
     assert (
-        results[desired_metric.id][0]
+        results[condition_metric.id][0]
         .reset_index(drop=True)
-        .equals(pd.Series([True, True, True, True]))
+        .equals(pd.Series([False, False, False, False]))
     )