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/dataset/pandas_dataset.py b/great_expectations/dataset/pandas_dataset.py
--- a/great_expectations/dataset/pandas_dataset.py
+++ b/great_expectations/dataset/pandas_dataset.py
@@ -1786,3 +1786,26 @@ def expect_multicolumn_values_to_be_unique(
         threshold = len(column_list.columns)
         # Do not dropna here, since we have separately dealt with na in decorator
         return column_list.nunique(dropna=False, axis=1) >= threshold
+
+    @DocInherit
+    @MetaPandasDataset.multicolumn_map_expectation
+    def expect_multicolumn_sum_to_equal(
+        self,
+        column_list,
+        sum_total,
+        result_format=None,
+        include_config=True,
+        catch_exceptions=None,
+        meta=None,
+    ):
+        """ Multi-Column Map Expectation
+
+        Expects that sum of all rows for a set of columns is equal to a specific value
+
+        Args:
+            column_list (List[str]): \
+                Set of columns to be checked
+            sum_total (int): \
+                expected sum of columns
+        """
+        return column_list.sum(axis=1) == sum_total
diff --git a/great_expectations/dataset/sparkdf_dataset.py b/great_expectations/dataset/sparkdf_dataset.py
--- a/great_expectations/dataset/sparkdf_dataset.py
+++ b/great_expectations/dataset/sparkdf_dataset.py
@@ -36,6 +36,7 @@
         isnan,
         datediff,
         lag,
+        expr,
     )
     import pyspark.sql.types as sparktypes
     from pyspark.ml.feature import Bucketizer
@@ -1479,3 +1480,33 @@ def expect_column_values_to_be_decreasing(
             return column.withColumn(
                 "__success", when(col("diff") <= 0, lit(True)).otherwise(lit(False))
             )
+
+    @DocInherit
+    @MetaSparkDFDataset.multicolumn_map_expectation
+    def expect_multicolumn_sum_to_equal(
+        self,
+        column_list,
+        sum_total,
+        result_format=None,
+        include_config=True,
+        catch_exceptions=None,
+        meta=None,
+    ):
+        """ Multi-Column Map Expectation
+
+        Expects that sum of all rows for a set of columns is equal to a specific value
+
+        Args:
+            column_list (List[str]): \
+                Set of columns to be checked
+            sum_total (int): \
+                expected sum of columns
+        """
+        expression = "+".join(
+            ["COALESCE({}, 0)".format(col) for col in column_list.columns]
+        )
+        column_list = column_list.withColumn("actual_total", expr(expression))
+        return column_list.withColumn(
+            "__success",
+            when(col("actual_total") == sum_total, lit(True)).otherwise(lit(False)),
+        )


Test patch:
diff --git a/tests/data_asset/test_data_asset.py b/tests/data_asset/test_data_asset.py
--- a/tests/data_asset/test_data_asset.py
+++ b/tests/data_asset/test_data_asset.py
@@ -134,6 +134,7 @@ def test_valid_expectation_types(dataset, pandas_dataset):
         "expect_column_values_to_not_be_null",
         "expect_column_values_to_not_match_regex",
         "expect_column_values_to_not_match_regex_list",
+        "expect_multicolumn_sum_to_equal",
         "expect_multicolumn_values_to_be_unique",
         "expect_table_column_count_to_be_between",
         "expect_table_column_count_to_equal",
diff --git a/tests/test_definitions/multicolumn_map_expectations/expect_multicolumn_sum_to_equal.json b/tests/test_definitions/multicolumn_map_expectations/expect_multicolumn_sum_to_equal.json
new file mode 100644
--- /dev/null
+++ b/tests/test_definitions/multicolumn_map_expectations/expect_multicolumn_sum_to_equal.json
@@ -0,0 +1,65 @@
+{
+  "expectation_type" : "expect_multicolumn_sum_to_equal",
+  "datasets" : [{
+    "data" : {
+      "w": [0.2, 0.4, 0.3, 0.5, 0.8, 0.7, 0.71, 0.56, 0.45, 1.0],
+      "x": [0.8, 0.6, 0.7, 0.5, 0.2, 0.3, 0.29, 0.44, 0.55, null],
+      "y": [0.8, 0.8, 0.7, 0.5, 0.4, 0.3, 0.29, 0.48, 0.55, null],
+      "z": [1, 2, -1, 3, 0, 2, 2, 0, 1, 3],
+      "a": [1, 0, 4, 0, 1, 0, 1, 3, 2, -3],
+      "b": [1, 1, 0, 0, 2, 1, 0, 0, 0, 3]
+    },
+    "tests" : [{
+      "title": "Basic positive test",
+      "exact_match_out" : false,
+      "in": {
+        "column_list": ["w", "x"],
+        "sum_total": 1
+      },
+      "out": {
+        "unexpected_list": [],
+        "unexpected_index_list": [],
+        "success": true
+      }
+    },{
+      "title": "Basic positive test - more than 2 columns",
+      "exact_match_out" : false,
+      "in": {
+        "column_list": ["z", "a", "b"],
+        "sum_total": 3
+      },
+      "out": {
+        "unexpected_list": [],
+        "unexpected_index_list": [],
+        "success": true
+      }
+    },{
+      "title": "Basic Negative test",
+      "exact_match_out" : false,
+      "in": {
+        "column_list": ["w", "y"],
+        "sum_total": 1
+      },
+      "out": {
+        "unexpected_list": [{"w": 0.4, "y": 0.8}, {"w": 0.8, "y": 0.4}, {"w": 0.56, "y": 0.48}],
+        "unexpected_index_list": [1,4,7],
+        "success": false
+      }
+    },{
+      "title": "Negative test - Different Value",
+      "exact_match_out" : false,
+      "in": {
+        "column_list": ["w", "x"],
+        "sum_total": 1.1
+      },
+      "out": {
+        "unexpected_list": [{"w": 0.2, "x": 0.8}, {"w": 0.4, "x": 0.6}, {"w": 0.3, "x": 0.7}, {"w": 0.5, "x": 0.5},
+          {"w": 0.8, "x": 0.2}, {"w": 0.7, "x": 0.3}, {"w": 0.71, "x": 0.29}, {"w": 0.56, "x": 0.44},
+          {"w": 0.45, "x": 0.55}, {"w": 1, "x": null}
+        ],
+        "unexpected_index_list": [0,1,2,3,4,5,6,7,8,9],
+        "success": false
+      }
+    }]
+  }]
+}
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -644,6 +644,7 @@ def candidate_test_is_on_temporary_notimplemented_list(context, expectation_type
             "expect_column_pair_values_to_be_in_set",
             "expect_multicolumn_values_to_be_unique",
             # "expect_table_row_count_to_equal_other_table",
+            "expect_multicolumn_sum_to_equal",
         ]
     if context == "SparkDFDataset":
         return expectation_type in [
@@ -693,6 +694,7 @@ def candidate_test_is_on_temporary_notimplemented_list(context, expectation_type
             "expect_column_pair_values_to_be_in_set",
             # "expect_multicolumn_values_to_be_unique",
             "expect_table_row_count_to_equal_other_table",
+            # "expect_multicolumn_sum_to_equal",
         ]
     if context == "PandasDataset":
         return expectation_type in [