File size: 4,962 Bytes
6d95c4c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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]