import os import json from typing import List import pandas as pd import numpy as np from keras.models import load_model from . import constants from . import utils class Prescriptor: """ Wrapper for Keras prescriptor and encoder. """ def __init__(self, prescriptor_id: str): """ :param prescriptor_id: ID of Keras prescriptor to load. """ prescriptor_model_filename = os.path.join(constants.PRESCRIPTOR_PATH, prescriptor_id + '.h5') self.prescriptor_model = load_model(prescriptor_model_filename, compile=False) self.encoder = None with open(constants.FIELDS_PATH, 'r') as f: fields = json.load(f) self.encoder = utils.Encoder(fields) def _is_single_action_prescriptor(self, actions): """ Checks how many Actions have been defined in the Context, Actions, Outcomes mapping. :return: True if only 1 action is defined, False otherwise """ return len(actions) == 1 def _is_scalar(self, prescribed_action): """ Checks if the prescribed action contains a single value, i.e. a scalar, or an array. A prescribed action contains a single value if it has been prescribed for a single context sample :param prescribed_action: a scalar or an array :return: True if the prescribed action contains a scalar, False otherwise. """ return prescribed_action.shape[0] == 1 and prescribed_action.shape[1] == 1 def _convert_to_nn_input(self, context_df: pd.DataFrame) -> List[np.ndarray]: """ Converts a context DataFrame to a list of numpy arrays a neural network can ingest :param context_df: a DataFrame containing inputs for a neural network. Number of inputs and size must match :return: a list of numpy ndarray, on ndarray per neural network input """ # The NN expects a list of i inputs by s samples (e.g. 9 x 299). # So convert the data frame to a numpy array (gives shape 299 x 9), transpose it (gives 9 x 299) # and convert to list(list of 9 arrays of 299) context_as_nn_input = list(context_df.to_numpy().transpose()) # Convert each column's list of 1D array to a 2D array context_as_nn_input = [np.stack(context_as_nn_input[i], axis=0) for i in range(len(context_as_nn_input))] return context_as_nn_input def __prescribe_from_model(self, context_df: pd.DataFrame) -> pd.DataFrame: """ Generates prescriptions using the passed neural network candidate and context ::param context_df: a DataFrame containing the context to prescribe for, :return: a pandas DataFrame of action name to action value or list of action values """ action_list = ['reco_land_use'] # Convert the input df context_as_nn_input = self._convert_to_nn_input(context_df) row_index = context_df.index # Get the prescrib?ed actions prescribed_actions = self.prescriptor_model.predict(context_as_nn_input) actions = {} if self._is_single_action_prescriptor(action_list): # Put the single action in an array to process it like multiple actions prescribed_actions = [prescribed_actions] for idx, action_col in enumerate(action_list): if self._is_scalar(prescribed_actions[idx]): # We have a single row and this action is numerical. Convert it to a scalar. actions[action_col] = prescribed_actions[idx].item() else: actions[action_col] = prescribed_actions[idx].tolist() # Convert the prescribed actions to a DataFrame prescribed_actions_df = pd.DataFrame(actions, columns=action_list, index=row_index) return prescribed_actions_df def run_prescriptor(self, sample_context_df): """ Runs prescriptor on context. Then re-scales prescribed land use to match how much was used in the sample. :param sample_context_df: a DataFrame containing the context :return: DataFrame of prescribed land use """ encoded_sample_context_df = self.encoder.encode_as_df(sample_context_df) prescribed_actions_df = self.__prescribe_from_model(encoded_sample_context_df) reco_land_use_df = pd.DataFrame(prescribed_actions_df["reco_land_use"].tolist(), columns=constants.RECO_COLS) # Re-scales our prescribed land to match the amount of land used in the sample used = sample_context_df[constants.RECO_COLS].iloc[0].sum() reco_land_use_df = reco_land_use_df[constants.RECO_COLS].mul(used, axis=0) # Reorder columns return reco_land_use_df[constants.RECO_COLS]