Repository: mitre/menelaus (language: python)

Issue:
Add Fundamental Feature Manipulation `Injector`s
For simple things like mean-shift, etc., in a single feature or multiple columns. Possibly related work in NNDVI code re: fitting distributions.


Discussion / hints:

Fix patch:
diff --git a/menelaus/injection/__init__.py b/menelaus/injection/__init__.py
index f550def4..6aad29fd 100644
--- a/menelaus/injection/__init__.py
+++ b/menelaus/injection/__init__.py
@@ -5,6 +5,7 @@
 from menelaus.injection.feature_manipulation import (
     FeatureSwapInjector,
     FeatureCoverInjector,
+    FeatureShiftInjector,
 )
 from menelaus.injection.injector import Injector
 from menelaus.injection.label_manipulation import (
diff --git a/menelaus/injection/feature_manipulation.py b/menelaus/injection/feature_manipulation.py
index 9da8e6df..87e75f6f 100644
--- a/menelaus/injection/feature_manipulation.py
+++ b/menelaus/injection/feature_manipulation.py
@@ -1,6 +1,45 @@
+import numpy as np
+
 from menelaus.injection.injector import Injector
 
 
+class FeatureShiftInjector(Injector):
+    """
+    Shifts a column in a dataset by increasing it according to a formula:
+    * ``column = column + (shift_factor * (alpha + mean_column))``
+
+    The alpha is a small value used to inject drift even if the mean is 0.
+    """
+
+    def __call__(self, data, from_index, to_index, col, shift_factor, alpha=0.001):
+        """
+        Args:
+            data (np.ndarray or pd.DataFrame): data to inject with drift
+            from_index (int): row index at which to start shift
+            to_index (int): row index at which to end (non-inclusive) shift
+            col (int or str): column index/name of column to shift
+            shift_factor (float): percentage of mean by which to shift data
+            alpha (float): small initial value to add to shift amount, in case mean is 0. Default 0.001
+
+        Returns:
+            np.ndarray or pd.DataFrame: copy of data, with two columns swapped
+                over given indices
+        """
+        # handle type
+        ret, (col,) = self._preprocess(data, col)
+
+        # add shift
+        self._section_mean = np.mean(ret[from_index:to_index, col])
+        self._delta = (alpha + self._section_mean) * shift_factor
+        ret[from_index:to_index, col] = np.add(
+            ret[from_index:to_index, col], self._delta
+        )
+
+        # handle type and return
+        ret = self._postprocess(ret)
+        return ret
+
+
 class FeatureSwapInjector(Injector):
     """
     Swaps two features/columns of a given dataset with each other.


Test patch:
diff --git a/tests/menelaus/injection/test_feature_manipulation.py b/tests/menelaus/injection/test_feature_manipulation.py
index a4df9df5..29f1cef3 100644
--- a/tests/menelaus/injection/test_feature_manipulation.py
+++ b/tests/menelaus/injection/test_feature_manipulation.py
@@ -1,6 +1,6 @@
 import numpy as np
 
-from menelaus.injection import FeatureSwapInjector, FeatureCoverInjector
+from menelaus.injection import FeatureSwapInjector, FeatureCoverInjector, FeatureShiftInjector
 
 
 def test_feature_swap_1():
@@ -19,3 +19,12 @@ def test_feature_cover_1():
     assert copy.shape == (4,1)
     assert np.array_equal(np.where(copy[:,0]==2)[0], [0,1])
     assert np.array_equal(np.where(copy[:,0]==3)[0], [2,3])
+
+def test_feature_shift_1():
+    ''' Check correct feature cover behavior '''
+    data = np.array([[0.0], [0.0], [3.0]])
+    i = FeatureShiftInjector()
+    copy = i(data=data, from_index=0, to_index=3, col=0, shift_factor=0.5, alpha=0.001)
+    # delta = 0.5(0.001 + mean) = 0.5(1.001) = 0.5005
+    exp = np.array([[0.5005], [0.5005], [3.5005]])
+    assert np.allclose(copy, exp)
\ No newline at end of file