Skip to content

Commit

Permalink
18691: Add Reaction to replace CasesWithDetails as react return type,…
Browse files Browse the repository at this point in the history
… Major (#115)

- Rename CasesWithDetails to Reaction
- Added the ability to instantiate the class with more than just a
dataframe.
- Extended the types allowed for the dispatch method add_reaction to
list of dicts and other classes of `Reaction`
- Removed support for add_reaction types of the old reaction type so now
everything should be in this class. Also it had errors when trying to
add_reaction other dicts due to the dispatch sending it to the wrong
method
  • Loading branch information
jackx111 authored Jan 26, 2024
1 parent 3299312 commit f4c6fe0
Show file tree
Hide file tree
Showing 11 changed files with 461 additions and 124 deletions.
6 changes: 4 additions & 2 deletions howso/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
TYPE_CHECKING,
Union,
)

from howso.utilities.reaction import Reaction
from pandas import DataFrame, Index

if TYPE_CHECKING:
Expand Down Expand Up @@ -276,7 +278,7 @@ def react_series(
use_case_weights=False,
use_regional_model_residuals=True,
weight_feature=None
) -> Union["ReactionSeries", Dict]:
) -> Reaction:
"""React in a series until a stop condition is met."""

@abstractmethod
Expand Down Expand Up @@ -368,7 +370,7 @@ def react(
use_case_weights=False,
use_regional_model_residuals=True,
weight_feature=None
) -> Union["Reaction", Dict]:
) -> Reaction:
"""Send a `react` to the Howso engine."""

@abstractmethod
Expand Down
39 changes: 20 additions & 19 deletions howso/client/pandas/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
format_dataframe,
)
from howso.utilities.internals import deserialize_to_dataframe
from howso.utilities.reaction import Reaction
import pandas as pd
from pandas import DataFrame, Index

Expand Down Expand Up @@ -206,7 +207,7 @@ def react_series(
*args,
series_index: str = '.series',
**kwargs
) -> Dict[str, Union[DataFrame, Dict]]:
) -> Reaction:
"""
Base: :func:`howso.client.AbstractHowsoClient.react_series`.
Expand All @@ -221,40 +222,40 @@ def react_series(
Returns
-------
dict
A dictionary with keys `series` and `explanation`. Where `series`
is a DataFrame of feature columns with series values as rows,
and `explanation` is a dict with the requested audit data.
Reaction:
A MutableMapping (dict-like) with these keys -> values:
action -> pandas.DataFrame
A data frame of action values.
details -> Dict or List
An aggregated list of any requested details.
"""
trainee_id = self._resolve_trainee_id(trainee_id)
feature_attributes = self.trainee_cache.get(trainee_id).features
response = super().react_series(trainee_id, *args, **kwargs)

# If series_index is not a string, the intention is likely for it to not be included
if not isinstance(series_index, str):
series_index = None
response = super().react_series(trainee_id, *args, series_index=series_index, **kwargs)

# Build response DataFrame
df = build_react_series_df(response, series_index=series_index)
response['series'] = format_dataframe(df, feature_attributes)
response['series'] = format_dataframe(response.get("series"), feature_attributes)

return response

def react(self, trainee_id, *args, **kwargs) -> Dict[str, Union[DataFrame, Dict]]:
def react(self, trainee_id, *args, **kwargs) -> Reaction:
"""
Base: :func:`howso.client.AbstractHowsoClient.react`.
Returns
-------
dict
A dictionary with keys `action` and `explanation`. Where `action`
is a DataFrame of action_feature columns to action_value rows,
and `explanation` is a dict with the requested audit data.
Reaction:
A MutableMapping (dict-like) with these keys -> values:
action -> pandas.DataFrame
A data frame of action values.
details -> Dict or List
An aggregated list of any requested details.
"""
trainee_id = self._resolve_trainee_id(trainee_id)
feature_attributes = self.trainee_cache.get(trainee_id).features
response = super().react(trainee_id, *args, **kwargs)
columns = response['explanation'].get('action_features')
columns = response['details'].get('action_features')
response['action'] = deserialize_cases(response['action'], columns,
feature_attributes)
return response
Expand Down
35 changes: 26 additions & 9 deletions howso/client/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
Trainee,
TrainResponse,
)
from howso.utilities.reaction import Reaction
from howso.utilities.testing import get_configurationless_test_client, get_test_options
import numpy as np
import pandas as pd
Expand Down Expand Up @@ -312,12 +313,27 @@ def test_react(self, trainee):
features=df.columns.tolist())
response = self.client.react(trainee.id,
contexts=[["2020-10-12T10:10:10.333"]])
assert response['action'][0]['nom'] == "b"
assert isinstance(response, Reaction)
assert response['action']['nom'].iloc[0] == "b"

response = self.client.react(trainee.id, contexts=[["b"]],
context_features=["nom"],
action_features=["datetime"])
assert "2020-10-12T10:10:10.333000" in response['action'][0]['datetime']
assert "2020-10-12T10:10:10.333000" in response['action']['datetime'].iloc[0]

def test_react_series(self, trainee):
"""Test that react series works as expected."""
df = pd.DataFrame(data=np.asarray([
['a', 'b', 'c', 'd'],
['2020-9-12T9:09:09.123', '2020-10-12T10:10:10.333',
'2020-12-12T12:12:12.444', '2020-10-11T11:11:11.222']
]).transpose(), columns=['nom', 'datetime'])
self.client.train(trainee.id, cases=df.values.tolist(),
features=df.columns.tolist())
response = self.client.react_series(trainee.id,
contexts=[["2020-10-12T10:10:10.333"]])
assert isinstance(response, Reaction)
assert response['action']['nom'].iloc[0] == "b"


class TestClient:
Expand Down Expand Up @@ -424,7 +440,8 @@ def test_train_and_react(self, trainee):
cases = [['1', '2'], ['3', '4']]
self.client.train(trainee.id, cases, features=['penguin', 'play'])
react_response = self.client.react(trainee.id, contexts=[['1']])
assert react_response['action'][0]['play'] == '2'
assert isinstance(react_response, Reaction)
assert react_response['action']['play'].iloc[0] == '2'
case_response = self.client.get_cases(
trainee.id, session=self.client.active_session.id)
for case in case_response.cases:
Expand Down Expand Up @@ -536,7 +553,7 @@ def test_a_la_cart_data(self, trainee):
Test a-la-cart data.
Systematically test a la cart options to ensure only the specified
options are returned in the explanation data.
options are returned in the details data.
Parameters
----------
Expand Down Expand Up @@ -590,8 +607,8 @@ def test_a_la_cart_data(self, trainee):
for audit_detail_set, keys_to_expect in details_sets:
response = self.client.react(trainee.id, contexts=[['1']],
details=audit_detail_set)
explanation = response['explanation']
assert (all(explanation[key] is not None for key in keys_to_expect))
details = response['details']
assert (all(details[key] is not None for key in keys_to_expect))

def test_get_version(self):
"""Test get_version()."""
Expand Down Expand Up @@ -1077,7 +1094,7 @@ def test_set_and_get_params(self, trainee, trainee_builder):
contexts=[2, 2],
context_features=['sepal_length', 'sepal_width'],
action_features=['petal_length'],
)['action'][0]['petal_length']
)['action']['petal_length'].iloc[0]

# create another trainee
other_trainee = Trainee(
Expand All @@ -1099,7 +1116,7 @@ def test_set_and_get_params(self, trainee, trainee_builder):
contexts=[2, 2],
context_features=['sepal_length', 'sepal_width'],
action_features=['petal_length'],
)['action'][0]['petal_length']
)['action']['petal_length'].iloc[0]
assert first_pred != second_pred

# align parameters, make another prediction that should be the same
Expand All @@ -1109,7 +1126,7 @@ def test_set_and_get_params(self, trainee, trainee_builder):
contexts=[2, 2],
context_features=['sepal_length', 'sepal_width'],
action_features=['petal_length'],
)['action'][0]['petal_length']
)['action']['petal_length'].iloc[0]
assert first_pred == third_pred

# verify that both trainees have the same hyperparameter_map now
Expand Down
84 changes: 42 additions & 42 deletions howso/direct/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
TraineeVersion
)
from howso.utilities import (
build_react_series_df,
internals,
num_list_dimensions,
ProgressTimer,
Expand All @@ -59,6 +60,8 @@
MultiTableFeatureAttributes,
SingleTableFeatureAttributes,
)
from howso.utilities.reaction import Reaction

import numpy as np
from packaging.version import parse as parse_version
from pandas import DataFrame
Expand Down Expand Up @@ -1915,13 +1918,14 @@ def react_series( # noqa: C901
series_context_features: Optional[Iterable[str]] = None,
series_context_values: Optional[Union[List[object], List[List[object]]]] = None,
series_id_tracking: Literal["dynamic", "fixed", "no"] = "fixed",
series_index: Optional[str] = None,
series_stop_maps: Optional[List[Dict[str, Dict]]] = None,
substitute_output: bool = True,
suppress_warning: bool = False,
use_case_weights: bool = False,
use_regional_model_residuals: bool = True,
weight_feature: Optional[str] = None
) -> Dict:
) -> Reaction:
"""
React in a series until a series_stop_map condition is met.
Expand Down Expand Up @@ -2033,7 +2037,10 @@ def react_series( # noqa: C901
allowed to change the series ID that it tracks based on its
current context.
- If "no", does not track any particular series ID.
series_index : str, Optional
When set to a string, will include the series index as a
column in the returned DataFrame using the column name given.
If set to None, no column will be added.
progress_callback : callable, optional
A callback method that will be called before each
batched call to react series and at the end of reacting. The method
Expand Down Expand Up @@ -2080,23 +2087,13 @@ def react_series( # noqa: C901
Returns
-------
dict
A dictionary with keys `action_features` and `series`. Where
`series` is a 2d list of values (rows of data per series), and
`action_features` is the list of all action features
(specified and derived).
Example output for 2 short series with 3 features:
Reaction:
A MutableMapping (dict-like) with these keys -> values:
action -> pandas.DataFrame
A data frame of action values.
.. code-block::
{
'action_features': ['id','x','y'],
'series': [
[ ["A", 1, 2], ["A", 2, 2] ],
[ ["B", 4, 4], ["B", 6, 7], ["B", 8, 9] ]
]
}
details -> Dict or List
An aggregated list of any requested details.
Raises
------
Expand Down Expand Up @@ -2344,11 +2341,11 @@ def react_series( # noqa: C901
if isinstance(progress_callback, Callable):
progress_callback(progress, response)

# put all explanations under the 'explanation' key
# put all details under the 'details' key
series = response.pop('series')
response = {'series': series, 'explanation': response}
response = {'series': series, 'details': response}

# If the number of series generated is less then requested, raise
# If the number of series generated is less then requested, raise
# warning, for generative reacts
if desired_conviction is not None:
len_action = len(response['series'])
Expand All @@ -2357,6 +2354,10 @@ def react_series( # noqa: C901
suppress_warning=suppress_warning
)

series_df = build_react_series_df(response, series_index=series_index)

response = Reaction(series_df, response.get('details'))

return response

def _batch_react_series( # noqa: C901
Expand Down Expand Up @@ -2486,7 +2487,7 @@ def _react_series(self, trainee_id, react_params):
ret['action_features'] = batch_result.pop('action_features') or []
ret['series'] = batch_result.pop('series')

# ensure all the explanation items are output as well
# ensure all the details items are output as well
for k, v in batch_result.items():
ret[k] = v or []

Expand Down Expand Up @@ -2579,7 +2580,7 @@ def react( # noqa: C901
use_case_weights: bool = False,
use_regional_model_residuals: bool = True,
weight_feature: Optional[str] = None,
) -> Dict:
) -> Reaction:
r"""
React to supplied values and cases contained within the Trainee.
Expand Down Expand Up @@ -2994,21 +2995,13 @@ def react( # noqa: C901
Returns
-------
dict
A dictionary with keys `action` and `explanation`. Where `action`
is a list of dicts of action_features -> action_values, and
`explanation` is a dict with the requested audit data.
.. code-block::
:caption: Example reaction for 2 contexts with 2 action features:
Reaction:
A MutableMapping (dict-like) with these keys -> values:
action -> pandas.DataFrame
A data frame of action values.
{
'action': [{'size': 1, 'width': 1}, {'size': 2, 'width': 2}]
'explanation': {
'action_features': ['size', 'width'],
'distance_contribution': [3.45, 0.89],
}
}
details -> Dict or List
An aggregated list of any requested details.
Raises
------
Expand Down Expand Up @@ -3163,11 +3156,16 @@ def react( # noqa: C901
if self._should_react_batch(react_params, total_size):
# Run in batch
if self.verbose:
print('Batch reacting to context on trainee with id: '
f'{trainee_id}')
response = self._batch_react(trainee_id, react_params,
total_size=total_size,
progress_callback=progress_callback)
print(
'Batch reacting to context on trainee with id: '
f'{trainee_id}'
)
response = self._batch_react(
trainee_id,
react_params,
total_size=total_size,
progress_callback=progress_callback
)
else:
# Run as a single react request
if self.verbose:
Expand All @@ -3191,6 +3189,8 @@ def react( # noqa: C901
suppress_warning=suppress_warning
)

response = Reaction(response.get('action'), response.get('details'))

return response

def _batch_react( # noqa: C901
Expand Down
Loading

0 comments on commit f4c6fe0

Please sign in to comment.