class CCKHToolBox(CCKHConsts):
    LEVEL1 = 1
    LEVEL2 = 2
    LEVEL3 = 3

    def __init__(self, data_provider, output):
        self.k_engine = BaseCalculationsScript(data_provider, output)
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_type = self.store_info['store_type'].iloc[0]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.general_tools = CCKHGENERALToolBox(self.data_provider, self.output)
        self.template = CCKHTemplateConsts()
        self.kpi_static_data = self.get_kpi_static_data()
        self.kpi_results_queries = []
        self.commonV2 = CommonV2(self.data_provider)
        self.kpi_new_static_data = self.commonV2.get_new_kpi_static_data()
        self.manufacturer = int(self.data_provider.own_manufacturer.param_value.values[0])
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.external_targets = self.ps_data_provider.get_kpi_external_targets()
        self.assortment = Assortment(self.data_provider, self.output)
        self.templates_info = self.external_targets[self.external_targets[CCKHTemplateConsts.TEMPLATE_OPERATION] ==
                                                    CCKHTemplateConsts.BASIC_SHEET]
        self.visibility_info = self.external_targets[self.external_targets[CCKHTemplateConsts.TEMPLATE_OPERATION]
                                                     == CCKHTemplateConsts.VISIBILITY_SHEET]
        self.cooler_info = self.external_targets[self.external_targets[CCKHTemplateConsts.TEMPLATE_OPERATION]
                                                 == CCKHTemplateConsts.COOLER_SHEET]

    def get_kpi_static_data(self):
        """
        This function extracts the static KPI data and saves it into one global data frame.
        The data is taken from static.kpi / static.atomic_kpi / static.kpi_set.
        """
        query = CCKHQueries.get_all_kpi_data()
        kpi_static_data = pd.read_sql_query(query, self.rds_conn.db)
        return kpi_static_data

    def calculate_red_score(self):
        """
        This function calculates the KPI results.
        """
        scores_dict = {}
        results_list_new_db = []
        # assortments based calculations for availability
        availability_kpi_dict, availability_score_dict = self.get_availability_kpi_data()
        results_list_new_db.extend(availability_kpi_dict)
        scores_dict.update(availability_score_dict)
        # external target based calculations
        final_main_child = self.templates_info[self.templates_info['Tested KPI Group'] == self.RED_SCORE].iloc[0]
        all_kpi_dict, all_score_dict = self.get_all_kpi_data()
        results_list_new_db.extend(all_kpi_dict)
        scores_dict.update(all_score_dict)
        # aggregation to calculate red score
        max_points = sum([score[0] for score in scores_dict.values()])
        actual_points = sum([score[1] for score in scores_dict.values()])
        red_score = 0 if max_points == 0 else round((actual_points / float(max_points)) * 100, 2)
        set_fk = self.kpi_static_data['kpi_set_fk'].values[0]
        self.write_to_db_result(set_fk, (actual_points, max_points, red_score), level=self.LEVEL1)
        results_list_new_db.append(self.get_new_kpi_dict(self.get_new_kpi_fk(final_main_child), red_score,
                                                         red_score, actual_points, max_points,
                                                         target=max_points,
                                                         weight=actual_points,
                                                         identifier_result=self.RED_SCORE,
                                                         identifier_parent=CCKHConsts.WEB_HIERARCHY))
        results_list_new_db.append(self.get_new_kpi_dict(self.get_new_kpi_by_name(self.RED_SCORE), red_score,
                                                         red_score, actual_points, max_points,
                                                         target=max_points, weight=actual_points,
                                                         identifier_result=CCKHConsts.WEB_HIERARCHY))
        self.commonV2.save_json_to_new_tables(results_list_new_db)
        self.commonV2.commit_results_data()

    def get_availability_kpi_data(self):
        availability_results_list = []
        scores_dict = {}
        availability_assortment_df = self.assortment.calculate_lvl3_assortment()
        if availability_assortment_df.empty:
            Log.info("Availability KPI: session: {} does not have relevant assortments.".format(self.session_uid))
            return [], {}
        availability_kpi = self.kpi_new_static_data[self.kpi_new_static_data['type'].str.encode(
            HelperConsts.UTF8) == self.template.AVAILABILITY_KPI_TYPE.encode(HelperConsts.UTF8)].iloc[0]
        availability_new_kpi_fk = availability_kpi.pk
        scores = []
        # no need to validate_kpi_run because Availability is 1; seems it was added for the other KPIs
        kpi_availability_group = availability_assortment_df.groupby('kpi_fk_lvl2')
        for kpi_fk_lvl2, availability_kpi_df in kpi_availability_group:
            score, result, threshold = self.calculate_availability(availability_kpi_df)
            numerator, denominator, result_new_db = result, threshold, result
            if score is not False:
                if score is None:
                    points = 0
                else:
                    points = 1
                    scores.append((points, score))
                atomic_kpi_name = self.kpi_new_static_data[self.kpi_new_static_data['pk'] ==
                                                           kpi_fk_lvl2].iloc[0].type
                Log.info('Save availability atomic kpi: {}'.format(atomic_kpi_name))
                atomic_kpi = self.kpi_static_data[(self.kpi_static_data['kpi_name'].str.encode(HelperConsts.UTF8) ==
                                                   self.template.AVAILABILITY_KPI_TYPE.encode(HelperConsts.UTF8)) &
                                                  (self.kpi_static_data['atomic_kpi_name'] == atomic_kpi_name)]
                atomic_fk = atomic_kpi.iloc[0].atomic_kpi_fk
                self.write_to_db_result(atomic_fk, (score, result, threshold, points), level=self.LEVEL3)
                child_name = atomic_kpi.atomic_kpi_name.iloc[0]
                child_kpi_fk = self.get_new_kpi_by_name(child_name)  # kpi fk from new tables
                Log.info('Save availability for {} ID: {}'.format(child_name, child_kpi_fk))
                availability_results_list.append(self.get_new_kpi_dict(child_kpi_fk, result_new_db, score,
                                                                       numerator, denominator,
                                                                       weight=points, target=denominator,
                                                                       identifier_parent={
                                                                           'kpi_fk': availability_new_kpi_fk},
                                                                       ))
        max_points = sum([score[0] for score in scores])
        actual_points = sum([score[0] * score[1] for score in scores])
        percentage = 0 if max_points == 0 else round(
            (actual_points / float(max_points)) * 100, 2)

        kpi_fk = self.kpi_static_data[self.kpi_static_data['kpi_name'].str.encode(HelperConsts.UTF8) ==
                                      self.template.AVAILABILITY_KPI_TYPE.encode(HelperConsts.UTF8)][
            'kpi_fk'].values[0]
        self.write_to_db_result(kpi_fk, (actual_points, max_points,
                                         percentage), level=self.LEVEL2)
        scores_dict[self.template.AVAILABILITY_KPI_TYPE] = (max_points, actual_points)
        availability_results_list.append(self.get_new_kpi_dict(availability_new_kpi_fk, percentage, percentage,
                                                               actual_points, max_points,
                                                               target=max_points,
                                                               weight=actual_points,
                                                               identifier_result={
                                                                   'kpi_fk': availability_new_kpi_fk},
                                                               identifier_parent=self.RED_SCORE))
        return availability_results_list, scores_dict

    def get_all_kpi_data(self):
        results_list_new_db = []
        scores_dict = {}
        if self.templates_info.empty:
            Log.info("All KPI: session: {} doesnt have relevant external targets".format(self.session_uid))
            return [], {}
        main_children = self.templates_info[self.templates_info[self.template.KPI_GROUP] == self.RED_SCORE]
        for c in xrange(0, len(main_children)):
            main_child = main_children.iloc[c]
            main_child_kpi_fk = self.get_new_kpi_fk(main_child)  # kpi fk from new tables
            main_kpi_identifier = self.commonV2.get_dictionary(kpi_fk=main_child_kpi_fk)
            if self.validate_store_type(main_child):
                children = self.templates_info[self.templates_info
                                               [self.template.KPI_GROUP].str.encode(HelperConsts.UTF8) ==
                                               main_child[self.template.KPI_NAME].encode(HelperConsts.UTF8)]
                scores = []
                for i in xrange(len(children)):
                    child = children.iloc[i]
                    numerator, denominator, result_new_db, numerator_id = 0, 0, 0, None
                    kpi_weight = self.validate_kpi_run(child)
                    if kpi_weight is not False:
                        kpi_type = child[self.template.KPI_TYPE]
                        result = threshold = None
                        if kpi_type == self.SURVEY:
                            score, result, threshold, survey_answer_fk = self.check_survey(child)
                            threshold = None
                            numerator, denominator, result_new_db = 1, 1, score * 100
                            numerator_id = survey_answer_fk
                        elif kpi_type == self.SHARE_OF_SHELF:
                            score, result, threshold, result_new_db, numerator, denominator = \
                                self.calculate_share_of_shelf(child)
                        elif kpi_type == self.NUMBER_OF_SCENES:
                            scene_types = self.get_scene_types(child)
                            result = self.general_tools.calculate_number_of_scenes(
                                **{SCENE_TYPE_FIELD: scene_types})
                            numerator, denominator, result_new_db = result, 1, result
                            score = 1 if result >= 1 else 0
                        else:
                            Log.warning("KPI of type '{}' is not supported via assortments".format(kpi_type))
                            continue
                        if score is not False:
                            if score is None:
                                points = 0
                            else:
                                points = float(child[self.template.WEIGHT]
                                               ) if kpi_weight is True else kpi_weight
                                scores.append((points, score))
                            atomic_fk = self.get_atomic_fk(main_child, child)
                            self.write_to_db_result(
                                atomic_fk, (score, result, threshold, points), level=self.LEVEL3)
                            identifier_parent = main_kpi_identifier
                            child_name = '{}-{}'.format(child[self.template.TRANSLATION], 'Atomic') \
                                if main_child[self.template.KPI_NAME] == child[self.template.KPI_NAME] else child[
                                self.template.TRANSLATION]
                            child.set_value(self.template.TRANSLATION, child_name)
                            child_kpi_fk = self.get_new_kpi_fk(child)  # kpi fk from new tables
                            results_list_new_db.append(self.get_new_kpi_dict(child_kpi_fk, result_new_db, score,
                                                                             numerator, denominator,
                                                                             weight=points, target=denominator,
                                                                             identifier_parent=identifier_parent,
                                                                             numerator_id=numerator_id))
                max_points = sum([score[0] for score in scores])
                actual_points = sum([score[0] * score[1] for score in scores])
                percentage = 0 if max_points == 0 else round(
                    (actual_points / float(max_points)) * 100, 2)

                kpi_name = main_child[self.template.TRANSLATION]
                kpi_fk = self.kpi_static_data[self.kpi_static_data['kpi_name'].str.encode(HelperConsts.UTF8) ==
                                              kpi_name.encode(HelperConsts.UTF8)]['kpi_fk'].values[0]
                self.write_to_db_result(kpi_fk, (actual_points, max_points,
                                                 percentage), level=self.LEVEL2)
                scores_dict[kpi_name] = (max_points, actual_points)
                results_list_new_db.append(self.get_new_kpi_dict(main_child_kpi_fk, percentage, percentage,
                                                                 actual_points, max_points,
                                                                 target=max_points,
                                                                 weight=actual_points,
                                                                 identifier_result=main_kpi_identifier,
                                                                 identifier_parent=self.RED_SCORE))
        return results_list_new_db, scores_dict

    def validate_store_type(self, params):
        """
        This function checks whether or not a KPI is relevant for calculation, by the session's store type.
        """
        validation = False
        stores = params[self.template.STORE_TYPE]
        if not stores:
            validation = True
        elif isinstance(stores, (str, unicode)):
            if stores.upper() == self.template.ALL or self.store_type in stores.split(self.template.SEPARATOR):
                validation = True
        elif isinstance(stores, list):
            if self.store_type in stores:
                validation = True
        return validation

    def validate_kpi_run(self, params):
        """
        This function checks whether or not a KPI Atomic needs to be calculated, based on a customized template.
        """
        weight = params[self.template.WEIGHT]
        if str(weight).isdigit():
            validation = True
        else:
            kpi_group = params[self.template.KPI_GROUP]
            if kpi_group == 'Visibility':
                custom_template = self.visibility_info
            elif kpi_group in ('Ambient Space', 'Cooler Space'):
                custom_template = self.cooler_info
            else:
                return False
            condition = (custom_template[self.template.KPI_NAME] == params[self.template.KPI_NAME])
            if self.template.KPI_GROUP in custom_template.keys() and kpi_group != 'Visibility':
                condition &= (custom_template[self.template.KPI_GROUP]
                              == params[self.template.KPI_GROUP])
            kpi_data = custom_template[condition]
            if kpi_data.empty:
                return False
            try:
                weight = \
                    kpi_data[
                        kpi_data['store_type'].str.encode(HelperConsts.UTF8) == self.store_type.encode(
                            HelperConsts.UTF8)][
                        'Target'].values[0]
                validation = float(weight)
            except ValueError:
                validation = False
            except IndexError:
                Log.warning("{kpi}: No matching external targets for this session: {sess}".format(
                    kpi=kpi_group,
                    sess=self.session_uid))
                validation = False
        return validation

    def get_atomic_fk(self, pillar, params):
        """
        This function gets an Atomic KPI's FK out of the template data.
        """
        atomic_name = params[self.template.TRANSLATION]
        kpi_name = pillar[self.template.TRANSLATION]
        atomic_fk = self.kpi_static_data[(self.kpi_static_data['kpi_name'].str.encode(HelperConsts.UTF8) ==
                                          kpi_name.encode(HelperConsts.UTF8)) & (
                                                 self.kpi_static_data['atomic_kpi_name'].str.encode(
                                                     HelperConsts.UTF8) == atomic_name.encode(HelperConsts.UTF8))][
            'atomic_kpi_fk']
        if atomic_fk.empty:
            return None
        return atomic_fk.values[0]

    def get_new_kpi_fk(self, params):
        """
        This function gets an KPI's FK from new kpi table 'static.kpi_level_2' out of the template data .
        """
        kpi_name = params[self.template.TRANSLATION]
        return self.get_new_kpi_by_name(kpi_name)

    def get_new_kpi_by_name(self, kpi_name):
        kpi_fk = self.kpi_new_static_data[self.kpi_new_static_data['type'].str.encode(HelperConsts.UTF8) ==
                                          kpi_name.encode(HelperConsts.UTF8)]['pk']
        if kpi_fk.empty:
            return None
        return kpi_fk.values[0]

    def get_scene_types(self, params):
        """
        This function extracts the relevant scene types (==additional_attribute_1) from the template.
        """
        scene_types = params[self.template.SCENE_TYPE]
        if not scene_types or (isinstance(scene_types, (str, unicode)) and scene_types.upper() == self.template.ALL):
            return None
        return scene_types

    def calculate_availability(self, availability_kpi_df):
        """
        This function calculates Availability typed Atomics from a customized template, and saves the results to the DB.
        """
        all_targets = availability_kpi_df.target.unique()
        if not all_targets:
            return False, False, False
        target = float(all_targets[0])
        total_facings_count = availability_kpi_df.facings.sum()
        score = 1 if total_facings_count >= target else 0
        return score, total_facings_count, target

    def check_survey(self, params):
        """
        This function calculates Survey typed Atomics, and saves the results to the DB.
        """
        survey_id = int(float(params[self.template.SURVEY_ID]))
        target_answer = params[self.template.SURVEY_ANSWER]
        survey_answer, survey_answer_fk = self.general_tools.get_survey_answer(survey_data=('question_fk', survey_id))
        score = 1 if survey_answer == target_answer else 0
        return score, survey_answer, target_answer, survey_answer_fk

    def calculate_share_of_shelf(self, params):
        """
        This function calculates Facings Share of Shelf typed Atomics, and saves the results to the DB.
        """
        if params[self.template.SOS_NUMERATOR].startswith('~'):
            sos_filters = {params[self.template.SOS_ENTITY]: (params[self.template.SOS_NUMERATOR][1:],
                                                              self.general_tools.EXCLUDE_FILTER)}
        else:
            sos_filters = {params[self.template.SOS_ENTITY]: params[self.template.SOS_NUMERATOR]}
        general_filters = {}
        scene_types = self.get_scene_types(params)
        if isinstance(scene_types, (str, unicode)):
            scene_types = scene_types.split(self.template.SEPARATOR)
        if scene_types:
            general_filters[SCENE_TYPE_FIELD] = scene_types
        products_to_exclude = params[self.template.PRODUCT_TYPES_TO_EXCLUDE]
        if products_to_exclude:
            general_filters['product_type'] = (products_to_exclude.split(self.template.SEPARATOR),
                                               self.general_tools.EXCLUDE_FILTER)
        numerator_result = self.general_tools.calculate_availability(
            **dict(sos_filters, **general_filters))
        denominator_result = self.general_tools.calculate_availability(**general_filters)
        if denominator_result == 0:
            result = 0
        else:
            result = round((numerator_result / float(denominator_result)) * 100, 2)
        if params[self.template.TARGET]:
            target = float(params[self.template.TARGET]) * 100
            score = 1 if result >= target else 0
        else:
            score = target = None
        result_string = '{0}% ({1}/{2})'.format(result, int(numerator_result), int(denominator_result))
        return score, result_string, target, result, numerator_result, denominator_result

    def write_to_db_result(self, fk, score, level):
        """
        This function creates the result data frame of every KPI (atomic KPI/KPI/KPI set),
        and appends the insert SQL query into the queries' list, later to be written to the DB.
        """
        attributes = self.create_attributes_dict(fk, score, level)
        if level == self.LEVEL1:
            table = KPS_RESULT
        elif level == self.LEVEL2:
            table = KPK_RESULT
        elif level == self.LEVEL3:
            table = KPI_RESULT
        else:
            return
        query = insert(attributes, table)
        self.kpi_results_queries.append(query)

    def create_attributes_dict(self, fk, score, level):
        """
        This function creates a data frame with all attributes needed for saving in KPI results tables.

        """
        if level == self.LEVEL1:
            score_2, score_3, score_1 = score
            kpi_set_name = self.kpi_static_data[self.kpi_static_data['kpi_set_fk']
                                                == fk]['kpi_set_name'].values[0]
            attributes = pd.DataFrame([(kpi_set_name, self.session_uid, self.store_id, self.visit_date.isoformat(),
                                        format(score_1, '.2f'), score_2, score_3, fk)],
                                      columns=['kps_name', 'session_uid', 'store_fk', 'visit_date', 'score_1',
                                               'score_2', 'score_3', 'kpi_set_fk'])
        elif level == self.LEVEL2:
            score_2, score_3, score = score
            kpi_name = self.kpi_static_data[self.kpi_static_data['kpi_fk']
                                            == fk]['kpi_name'].values[0]
            attributes = pd.DataFrame([(self.session_uid, self.store_id, self.visit_date.isoformat(),
                                        fk, kpi_name, score, score_2, score_3)],
                                      columns=['session_uid', 'store_fk', 'visit_date', 'kpi_fk', 'kpk_name',
                                               'score', 'score_2', 'score_3'])
        elif level == self.LEVEL3:
            score, result, threshold, weight = score
            data = self.kpi_static_data[self.kpi_static_data['atomic_kpi_fk'] == fk]
            atomic_kpi_name = data['atomic_kpi_name'].values[0]
            kpi_fk = data['kpi_fk'].values[0]
            kpi_set_name = self.kpi_static_data[self.kpi_static_data['atomic_kpi_fk']
                                                == fk]['kpi_set_name'].values[0]
            attributes = pd.DataFrame([(atomic_kpi_name, self.session_uid, kpi_set_name, self.store_id,
                                        self.visit_date.isoformat(), datetime.utcnow().isoformat(),
                                        score, kpi_fk, fk, threshold, result, weight)],
                                      columns=['display_text', 'session_uid', 'kps_name', 'store_fk', 'visit_date',
                                               'calculation_time', 'score', 'kpi_fk', 'atomic_kpi_fk', 'threshold',
                                               'result', 'kpi_weight'])
        else:
            attributes = pd.DataFrame()
        return attributes.to_dict()

    @log_runtime('Saving to DB')
    def commit_results_data(self):
        """
        This function writes all KPI results to the DB, and commits the changes.
        """
        cur = self.rds_conn.db.cursor()
        delete_queries = CCKHQueries.get_delete_session_results_query(self.session_uid)
        for query in delete_queries:
            cur.execute(query)
        for query in self.kpi_results_queries:
            cur.execute(query)
        self.rds_conn.db.commit()

    def get_new_kpi_dict(self, kpi_fk, result, score, numerator_result, denominator_result,
                         score_after_action=0, weight=None, target=None, identifier_parent=None,
                         identifier_result=None, numerator_id=None):

        """
        This function gets all kpi info  and add the relevant numerator_id and denominator_id and return a dictionary
        with the passed data.
             :param kpi_fk: pk of kpi
             :param result
             :param score
             :param numerator_result
             :param denominator_result
             :param weight
             :param target
             :param identifier_parent
             :param identifier_result
             :param numerator_id
             :param score_after_action

             :returns dict in format of db result
        """
        numerator_id = self.manufacturer if numerator_id is None else numerator_id
        denominator_id = self.store_id
        return {'fk': kpi_fk,
                SessionResultsConsts.NUMERATOR_ID: numerator_id,
                SessionResultsConsts.DENOMINATOR_ID: denominator_id,
                SessionResultsConsts.DENOMINATOR_RESULT: denominator_result,
                SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
                SessionResultsConsts.RESULT: result, SessionResultsConsts.SCORE: score,
                SessionResultsConsts.TARGET: target, SessionResultsConsts.WEIGHT: weight,
                'identifier_parent': identifier_parent, 'identifier_result': identifier_result,
                'score_after_actions': score_after_action, 'should_enter': True,
                }
class PSAPAC_SAND3ToolBox:
    # Gsk Japan kpis

    # DEFAULT_TARGET = {ProductsConsts.BRAND_FK: [-1], 'shelves': ["1,2,3"], 'block_target': [80], 'brand_target': [100], 'position_target': [80]}

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = Common(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.kpi_results_queries = []
        self.set_up_template = pd.read_excel(os.path.join(
            os.path.dirname(os.path.realpath(__file__)), '..', 'Data',
            'gsk_set_up.xlsx'),
                                             sheet_name='Functional KPIs',
                                             keep_default_na=False)

        self.gsk_generator = GSKGenerator(self.data_provider, self.output,
                                          self.common, self.set_up_template)
        self.blocking_generator = Block(self.data_provider)
        self.assortment = self.gsk_generator.get_assortment_data_provider()
        self.store_info = self.data_provider['store_info']
        self.store_fk = self.data_provider[StoreInfoConsts.STORE_FK]
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.targets = self.ps_data_provider.get_kpi_external_targets(
            key_fields=Consts.KEY_FIELDS, data_fields=Consts.DATA_FIELDS)
        self.own_manufacturer = self.get_manufacturer
        self.set_up_data = {
            (Consts.PLN_BLOCK, Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            (Consts.POSITION_SCORE, Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            (Consts.ECAPS_FILTER_IDENT, Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            (Consts.PLN_MSL, Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            ("GSK_PLN_LSOS_SCORE", Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            (Consts.POSM, Const.KPI_TYPE_COLUMN): Const.NO_INFO
        }

    @property
    def get_manufacturer(self):
        return int(self.data_provider.own_manufacturer[
            self.data_provider.own_manufacturer['param_name'] ==
            'manufacturer_id']['param_value'].iloc[0])

    def main_calculation(self, *args, **kwargs):
        """
        This function calculates the KPI results.Global functions and local functions
        """
        # global kpis

        assortment_store_dict = self.gsk_generator.availability_store_function(
        )
        self.common.save_json_to_new_tables(assortment_store_dict)

        assortment_category_dict = self.gsk_generator.availability_category_function(
        )
        self.common.save_json_to_new_tables(assortment_category_dict)

        assortment_subcategory_dict = self.gsk_generator.availability_subcategory_function(
        )
        self.common.save_json_to_new_tables(assortment_subcategory_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_sub_category_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_category_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_whole_store_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        # # local kpis
        for kpi in Consts.KPI_DICT.keys():
            self.gsk_generator.tool_box.extract_data_set_up_file(
                kpi, self.set_up_data, Consts.KPI_DICT)

        results_ecaps = self.gsk_ecaps_kpis()
        self.common.save_json_to_new_tables(results_ecaps)

        self.get_store_target()  # choosing the policy
        if self.targets.empty:
            Log.warning('There is no target policy matching this store')
        else:
            results_compliance = self.gsk_compliance()
            self.common.save_json_to_new_tables(results_compliance)

        results_pos = self.gsk_pos_kpis()
        self.common.save_json_to_new_tables(results_pos)

        self.common.commit_results_data()
        return

    def position_shelf(self, brand_fk, policy, df):
        """
        :param  brand_fk :
        :param  policy : dictionary that contains {
                                                'shelves':"1 ,2 ,4 ,5" (or any other string of numbers separate by ','),
                                                'position_target': 80 (or any other percentage you want the score to
                                                reach)
                                                }
        :param  df: data frame that contains columns MatchesConsts.SHELF_NUMBER , "brand kf"

        :returns   tuple of (result,score,numerator,denominator)
                   result = number of products from brand_fk in shelves / number of products from brand_fk ,
                   score  = if result reach position target 100  else 0 ,
                   numerator = number of products from brand_fk in shelves
                   denominator = number of products from brand_fk
        """
        if (Consts.SHELVES
                not in policy.keys()) or policy[Consts.SHELVES].empty:
            Log.warning(
                'This sessions have external targets but doesnt have value for shelves position'
            )
            return 0, 0, 0, 0, 0
        if isinstance(policy[Consts.SHELVES].iloc[0], list):
            shelf_from_bottom = [
                int(shelf) for shelf in policy[Consts.SHELVES].iloc[0]
            ]
        else:
            shelf_from_bottom = [
                int(shelf)
                for shelf in policy[Consts.SHELVES].iloc[0].split(",")
            ]

        threshold = policy[Consts.POSITION_TARGET].iloc[0]
        brand_df = df[df[ProductsConsts.BRAND_FK] == brand_fk]
        shelf_df = brand_df[brand_df[MatchesConsts.SHELF_NUMBER].isin(
            shelf_from_bottom)]
        numerator = shelf_df.shape[0]
        denominator = brand_df.shape[0]
        result = float(numerator) / float(denominator)
        score = 1 if (result * 100) >= threshold else 0
        return result, score, numerator, denominator, threshold

    def lsos_score(self, brand, policy):
        """
        :param brand : pk of brand
        :param policy :  dictionary of  { 'brand_target' : lsos number you want to reach}
        This function uses the lsos_in whole_store global calculation.
        it takes the result of the parameter 'brand' according to the policy set target and results.
        :return result,score,target
                result : result of this brand lsos
                score :  result / brand_target ,
                target  :  branf_target

        """
        df = pd.merge(self.match_product_in_scene,
                      self.all_products[Const.PRODUCTS_COLUMNS],
                      how='left',
                      on=[MatchesConsts.PRODUCT_FK])
        df = pd.merge(self.scif[Const.SCIF_COLUMNS],
                      df,
                      how='right',
                      right_on=[ScifConsts.SCENE_FK, ScifConsts.PRODUCT_FK],
                      left_on=[ScifConsts.SCENE_ID, ScifConsts.PRODUCT_FK])

        if df.empty:
            Log.warning('match_product_in_scene is empty ')
            return 0, 0, 0
        df = self.gsk_generator.tool_box.tests_by_template(
            'GSK_PLN_LSOS_SCORE', df, self.set_up_data)
        if df is None:
            Log.warning('match_product_in_scene is empty ')
            return 0, 0, 0
        result = self.gsk_generator.tool_box.calculate_sos(
            df, {ProductsConsts.BRAND_FK: brand}, {}, Const.LINEAR)[0]
        target = policy['brand_target'].iloc[0]
        score = float(result) / float(target)
        return result, score, target

    def brand_blocking(self, brand, policy):
        """
                :param brand : pk of brand
                :param policy :  dictionary of  { 'block_target' : number you want to reach}
                :return result : 1 if there is a block answer set_up_data conditions else 0
        """
        templates = self.set_up_data[(Const.SCENE_TYPE, Consts.PLN_BLOCK)]
        template_name = {
            ScifConsts.TEMPLATE_NAME: templates
        } if templates else None  # figure out which template name should I use
        ignore_empty = False
        # taking from params from set up  info
        stacking_param = False if not self.set_up_data[(
            Const.INCLUDE_STACKING, Consts.PLN_BLOCK)] else True  # false
        population_parameters = {
            ProductsConsts.BRAND_FK: [brand],
            ProductsConsts.PRODUCT_TYPE: [ProductTypeConsts.SKU]
        }

        if self.set_up_data[(Const.INCLUDE_OTHERS, Consts.PLN_BLOCK)]:
            population_parameters[ProductsConsts.PRODUCT_TYPE].append(
                Const.OTHER)
        if self.set_up_data[(Const.INCLUDE_IRRELEVANT, Consts.PLN_BLOCK)]:
            population_parameters[ProductsConsts.PRODUCT_TYPE].append(
                Const.IRRELEVANT)
        if self.set_up_data[(Const.INCLUDE_EMPTY, Consts.PLN_BLOCK)]:

            population_parameters[ProductsConsts.PRODUCT_TYPE].append(
                Const.EMPTY)
        else:
            ignore_empty = True

        if self.set_up_data[(Const.CATEGORY_INCLUDE,
                             Consts.PLN_BLOCK)]:  # category_name
            population_parameters[ProductsConsts.CATEGORY] = self.set_up_data[(
                Const.CATEGORY_INCLUDE, Consts.PLN_BLOCK)]

        if self.set_up_data[(Const.SUB_CATEGORY_INCLUDE,
                             Consts.PLN_BLOCK)]:  # sub_category_name
            population_parameters[
                ProductsConsts.SUB_CATEGORY] = self.set_up_data[(
                    Const.SUB_CATEGORY_INCLUDE, Consts.PLN_BLOCK)]

        # from Data file
        target = float(policy['block_target'].iloc[0]) / float(100)
        result = self.blocking_generator.network_x_block_together(
            location=template_name,
            population=population_parameters,
            additional={
                'minimum_block_ratio': target,
                'calculate_all_scenes': True,
                'ignore_empty': ignore_empty,
                'include_stacking': stacking_param,
                'check_vertical_horizontal': True,
                'minimum_facing_for_block': 1
            })
        result.sort_values('facing_percentage', ascending=False, inplace=True)
        score = 0 if result[result['is_block']].empty else 1
        numerator = 0 if result.empty else result['block_facings'].iloc[0]
        denominator = 0 if result.empty else result['total_facings'].iloc[0]

        return score, target, numerator, denominator

    def msl_assortment(self, kpi_fk, kpi_name):
        """
                        :param kpi_fk : name of level 3 assortment kpi
                        :param kpi_name: GSK_PLN_MSL_SCORE assortment , or   GSK_ECAPS assortment
                        :return kpi_results : data frame of assortment products of the kpi, product's availability,
                        product details.
                        filtered by set up
                """
        lvl3_assort, filter_scif = self.gsk_generator.tool_box.get_assortment_filtered(
            self.set_up_data, kpi_name)
        if lvl3_assort is None or lvl3_assort.empty:
            return None
        kpi_assortment_fk = self.common.get_kpi_fk_by_kpi_type(kpi_fk)
        kpi_results = lvl3_assort[lvl3_assort['kpi_fk_lvl3'] ==
                                  kpi_assortment_fk]  # general assortment
        kpi_results = pd.merge(kpi_results,
                               self.all_products[Const.PRODUCTS_COLUMNS],
                               how='left',
                               on=ProductsConsts.PRODUCT_FK)

        kpi_results = kpi_results[kpi_results[
            ProductsConsts.SUBSTITUTION_PRODUCT_FK].isnull()]
        return kpi_results

    def pln_ecaps_score(self, brand, assortment):
        """
                             :param brand : pk of desired brand
                             :param assortment : data frame of assortment products of the kpi, product's availability,
                                    product details. filtered by set up

                             besides result of lvl2_assortment function writing level 3 assortment product presence
                             results

                             :return  numerator : how many products available out of the granular groups
                                      denominator : how many products in assortment groups
                                      result :  (numerator/denominator)*100
                                      results :  array of dictionary, each dict contains the result details
        """
        identifier_parent = self.common.get_dictionary(
            brand_fk=brand,
            kpi_fk=self.common.get_kpi_fk_by_kpi_type(Consts.ECAP_ALL_BRAND))
        results = []
        kpi_ecaps_product = self.common.get_kpi_fk_by_kpi_type(
            Consts.PRODUCT_PRESENCE)
        ecaps_assortment_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.PLN_ASSORTMENT_KPI)
        if assortment.empty:
            return 0, 0, 0, results
        brand_results = assortment[assortment[ProductsConsts.BRAND_FK] ==
                                   brand]  # only assortment of desired brand
        for result in brand_results.itertuples():
            if (math.isnan(result.in_store)) | (result.kpi_fk_lvl3 !=
                                                ecaps_assortment_fk):
                score = self.gsk_generator.tool_box.result_value_pk(
                    Const.EXTRA)
                result_num = 1
            else:
                score = self.gsk_generator.tool_box.result_value_pk(Const.OOS) if result.in_store == 0 else \
                    self.gsk_generator.tool_box.result_value_pk(Const.DISTRIBUTED)
                result_num = result.in_store
            last_status = self.gsk_generator.tool_box.get_last_status(
                kpi_ecaps_product, result.product_fk)
            # score = result.in_store * 100
            results.append({
                'fk': kpi_ecaps_product,
                SessionResultsConsts.NUMERATOR_ID: result.product_fk,
                SessionResultsConsts.DENOMINATOR_ID: self.store_fk,
                SessionResultsConsts.DENOMINATOR_RESULT: 1,
                SessionResultsConsts.NUMERATOR_RESULT: result_num,
                SessionResultsConsts.RESULT: score,
                SessionResultsConsts.SCORE: last_status,
                'identifier_parent': identifier_parent,
                'identifier_result': 1,
                'should_enter': True
            })

        if 'total' not in self.assortment.LVL2_HEADERS or 'passes' not in self.assortment.LVL2_HEADERS:
            self.assortment.LVL2_HEADERS.extend(['total', 'passes'])
        lvl2 = self.assortment.calculate_lvl2_assortment(brand_results)
        if lvl2.empty:
            return 0, 0, 0, results  # in case of no assortment return 0
        result = round(
            np.divide(float(lvl2.iloc[0].passes), float(lvl2.iloc[0].total)),
            4)
        return lvl2.iloc[0].passes, lvl2.iloc[0].total, result, results

    def pln_msl_summary(self, brand, assortment):
        """
                :param brand : pk of desired brand
                :param assortment : data frame of assortment products of the kpi, product's availability,
                                           product details. filtered by set up
                :return  numerator : how many products available out of the granular groups
                                             denominator : how many products in assortment groups
                                             result :  (numerator/denominator)*100
                                             results :  array of dictionary, each dict contains the result details
               """

        if assortment is None or assortment.empty:
            return 0, 0, 0, 0
        brand_results = assortment[assortment[ProductsConsts.BRAND_FK] ==
                                   brand]  # only assortment of desired brand
        if 'total' not in self.assortment.LVL2_HEADERS or 'passes' not in self.assortment.LVL2_HEADERS:
            self.assortment.LVL2_HEADERS.extend(['total', 'passes'])

        lvl2 = self.assortment.calculate_lvl2_assortment(brand_results)
        if lvl2.empty:
            return 0, 0, 0, 0  # in case of no assortment return 0
        result = round(
            np.divide(float(lvl2.iloc[0].passes), float(lvl2.iloc[0].total)),
            4)
        return lvl2.iloc[0].passes, lvl2.iloc[0].total, result, lvl2.iloc[
            0].assortment_group_fk

    def get_store_target(self):
        """
            Function checks which policies out of self.target are relevant to this store visit according to store
            attributes.
        """

        parameters_dict = {StoreInfoConsts.STORE_NUMBER_1: 'store_number'}
        for store_param, target_param in parameters_dict.items():
            if target_param in self.targets.columns:
                if self.store_info[store_param][0] is None:
                    if self.targets.empty or self.targets[
                            self.targets[target_param] != ''].empty:
                        continue
                    else:
                        self.targets.drop(self.targets.index, inplace=True)
                self.targets = self.targets[
                    (self.targets[target_param] ==
                     self.store_info[store_param][0].encode(HelperConsts.UTF8))
                    | (self.targets[target_param] == '')]

    def gsk_compliance(self):
        """
                    Function calculate compliance score for each brand based on : 
                    position score, brand-assortment score,
                    block score ,lsos score.
                    Also calculate  compliance summary score  - average of brands compliance scores
                """
        results_df = []
        df = self.scif
        # kpis
        kpi_block_fk = self.common.get_kpi_fk_by_kpi_type(Consts.PLN_BLOCK)
        kpi_position_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.POSITION_SCORE)
        kpi_lsos_fk = self.common.get_kpi_fk_by_kpi_type(Consts.PLN_LSOS)
        kpi_msl_fk = self.common.get_kpi_fk_by_kpi_type(Consts.PLN_MSL)
        kpi_compliance_brands_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.COMPLIANCE_ALL_BRANDS)
        kpi_compliance_summary_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.COMPLIANCE_SUMMARY)
        identifier_compliance_summary = self.common.get_dictionary(
            kpi_fk=kpi_compliance_summary_fk)

        # targets
        block_target = 0.25
        posit_target = 0.25
        lsos_target = 0.25
        msl_target = 0.25

        total_brand_score = 0
        counter_brands = 0

        # assortment_lvl3 msl df initialize
        self.gsk_generator.tool_box.extract_data_set_up_file(
            Consts.PLN_MSL, self.set_up_data, Consts.KPI_DICT)
        assortment_msl = self.msl_assortment(Const.DISTRIBUTION,
                                             Consts.PLN_MSL)

        # set data frame to find position shelf
        df_position_score = pd.merge(self.match_product_in_scene,
                                     self.all_products,
                                     on=ProductsConsts.PRODUCT_FK)
        df_position_score = pd.merge(
            self.scif[Const.SCIF_COLUMNS],
            df_position_score,
            how='right',
            right_on=[ScifConsts.SCENE_FK, ProductsConsts.PRODUCT_FK],
            left_on=[ScifConsts.SCENE_ID, ScifConsts.PRODUCT_FK])
        df_position_score = self.gsk_generator.tool_box.tests_by_template(
            Consts.POSITION_SCORE, df_position_score, self.set_up_data)

        if not self.set_up_data[(Const.INCLUDE_STACKING,
                                 Consts.POSITION_SCORE)]:
            df_position_score = df_position_score if df_position_score is None else df_position_score[
                df_position_score[MatchesConsts.STACKING_LAYER] == 1]

        # calculate all brands if template doesnt require specific brand else only for specific brands
        template_brands = self.set_up_data[(Const.BRANDS_INCLUDE,
                                            Consts.PLN_BLOCK)]
        brands = df[df[ProductsConsts.BRAND_NAME].isin(template_brands)][ProductsConsts.BRAND_FK].unique() if \
            template_brands else df[ProductsConsts.BRAND_FK].dropna().unique()

        for brand in brands:
            policy = self.targets[self.targets[ProductsConsts.BRAND_FK] ==
                                  brand]
            if policy.empty:
                Log.warning('There is no target policy matching brand'
                            )  # adding brand name
                return results_df
            identifier_parent = self.common.get_dictionary(
                brand_fk=brand, kpi_fk=kpi_compliance_brands_fk)
            # msl_kpi
            msl_numerator, msl_denominator, msl_result, msl_assortment_group = self.pln_msl_summary(
                brand, assortment_msl)
            msl_score = msl_result * msl_target
            results_df.append({
                'fk': kpi_msl_fk,
                SessionResultsConsts.NUMERATOR_ID: brand,
                SessionResultsConsts.DENOMINATOR_ID: self.store_fk,
                SessionResultsConsts.DENOMINATOR_RESULT: msl_denominator,
                SessionResultsConsts.NUMERATOR_RESULT: msl_numerator,
                SessionResultsConsts.RESULT: msl_result,
                SessionResultsConsts.SCORE: msl_score,
                SessionResultsConsts.TARGET: msl_target,
                SessionResultsConsts.CONTEXT_ID: msl_assortment_group,
                'identifier_parent': identifier_parent,
                'should_enter': True
            })
            # lsos kpi
            lsos_numerator, lsos_result, lsos_denominator = self.lsos_score(
                brand, policy)
            lsos_result = 1 if lsos_result > 1 else lsos_result
            lsos_score = lsos_result * lsos_target
            results_df.append({
                'fk': kpi_lsos_fk,
                SessionResultsConsts.NUMERATOR_ID: brand,
                SessionResultsConsts.DENOMINATOR_ID: self.store_fk,
                SessionResultsConsts.DENOMINATOR_RESULT: lsos_denominator,
                SessionResultsConsts.NUMERATOR_RESULT: lsos_numerator,
                SessionResultsConsts.RESULT: lsos_result,
                SessionResultsConsts.SCORE: lsos_score,
                SessionResultsConsts.TARGET: lsos_target,
                'identifier_parent': identifier_parent,
                SessionResultsConsts.WEIGHT: lsos_denominator,
                'should_enter': True
            })
            # block_score
            block_result, block_benchmark, numerator_block, block_denominator = self.brand_blocking(
                brand, policy)
            block_score = round(block_result * block_target, 4)
            results_df.append({
                'fk':
                kpi_block_fk,
                SessionResultsConsts.NUMERATOR_ID:
                brand,
                SessionResultsConsts.DENOMINATOR_ID:
                self.store_fk,
                SessionResultsConsts.DENOMINATOR_RESULT:
                block_denominator,
                SessionResultsConsts.NUMERATOR_RESULT:
                numerator_block,
                SessionResultsConsts.RESULT:
                block_result,
                SessionResultsConsts.SCORE:
                block_score,
                SessionResultsConsts.TARGET:
                block_target,
                'identifier_parent':
                identifier_parent,
                'should_enter':
                True,
                SessionResultsConsts.WEIGHT: (block_benchmark * 100)
            })

            # position score
            if df_position_score is not None:
                position_result, position_score, position_num, position_den, position_benchmark = self.position_shelf(
                    brand, policy, df_position_score)
            else:
                position_result, position_score, position_num, position_den, position_benchmark = 0, 0, 0, 0, 0
            position_score = round(position_score * posit_target, 4)
            results_df.append({
                'fk': kpi_position_fk,
                SessionResultsConsts.NUMERATOR_ID: brand,
                SessionResultsConsts.DENOMINATOR_ID: self.store_fk,
                SessionResultsConsts.DENOMINATOR_RESULT: position_den,
                SessionResultsConsts.NUMERATOR_RESULT: position_num,
                SessionResultsConsts.RESULT: position_result,
                SessionResultsConsts.SCORE: position_score,
                SessionResultsConsts.TARGET: posit_target,
                'identifier_parent': identifier_parent,
                'should_enter': True,
                SessionResultsConsts.WEIGHT: position_benchmark
            })

            # compliance score per brand
            compliance_score = round(
                position_score + block_score + lsos_score + msl_score, 4)
            results_df.append({
                'fk': kpi_compliance_brands_fk,
                SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer,
                SessionResultsConsts.DENOMINATOR_ID: brand,
                SessionResultsConsts.DENOMINATOR_RESULT: 1,
                SessionResultsConsts.NUMERATOR_RESULT: compliance_score,
                SessionResultsConsts.RESULT: compliance_score,
                SessionResultsConsts.SCORE: compliance_score,
                'identifier_parent': identifier_compliance_summary,
                'identifier_result': identifier_parent,
                'should_enter': True
            })

            # counter and sum updates
            total_brand_score = round(total_brand_score + compliance_score, 4)
            counter_brands = counter_brands + 1
        if counter_brands == 0:
            return results_df
        # compliance summary
        average_brand_score = round(total_brand_score / counter_brands, 4)
        results_df.append({
            'fk': kpi_compliance_summary_fk,
            SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer,
            SessionResultsConsts.DENOMINATOR_ID: self.store_fk,
            SessionResultsConsts.DENOMINATOR_RESULT: counter_brands,
            SessionResultsConsts.NUMERATOR_RESULT: total_brand_score,
            SessionResultsConsts.RESULT: average_brand_score,
            SessionResultsConsts.SCORE: average_brand_score,
            'identifier_result': identifier_compliance_summary
        })

        return results_df

    def gsk_ecaps_kpis(self):
        """
                      Function calculate for each brand ecaps score, and for all brands together set ecaps summary score
                      :return
                             results_df :  array of dictionary, each dict contains kpi's result details
       """
        results_df = []
        kpi_ecaps_brands_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.ECAP_ALL_BRAND)
        kpi_ecaps_summary_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.ECAP_SUMMARY)
        identifier_ecaps_summary = self.common.get_dictionary(
            kpi_fk=kpi_ecaps_summary_fk)
        total_brand_score = 0
        assortment_display = self.msl_assortment(Consts.PLN_ASSORTMENT_KPI,
                                                 Consts.ECAPS_FILTER_IDENT)

        if assortment_display is None or assortment_display.empty:
            return results_df
        template_brands = self.set_up_data[(Const.BRANDS_INCLUDE,
                                            Consts.ECAPS_FILTER_IDENT)]
        brands = assortment_display[assortment_display[ProductsConsts.BRAND_NAME].isin(template_brands)][
            ProductsConsts.BRAND_FK].unique() if \
            template_brands else assortment_display[ProductsConsts.BRAND_FK].dropna().unique()

        for brand in brands:
            numerator_res, denominator_res, result, product_presence_df = self.pln_ecaps_score(
                brand, assortment_display)
            results_df.extend(product_presence_df)
            identifier_all_brand = self.common.get_dictionary(
                brand_fk=brand,
                kpi_fk=self.common.get_kpi_fk_by_kpi_type(
                    Consts.ECAP_ALL_BRAND))
            results_df.append({
                'fk': kpi_ecaps_brands_fk,
                SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer,
                SessionResultsConsts.DENOMINATOR_ID: brand,
                SessionResultsConsts.DENOMINATOR_RESULT: denominator_res,
                SessionResultsConsts.NUMERATOR_RESULT: numerator_res,
                SessionResultsConsts.RESULT: result,
                SessionResultsConsts.SCORE: result,
                'identifier_parent': identifier_ecaps_summary,
                'identifier_result': identifier_all_brand,
                'should_enter': True
            })

            total_brand_score = total_brand_score + result
        if len(
                brands
        ) > 0:  # don't want to show result in case of there are no brands relevan to the template
            result_summary = round(total_brand_score / len(brands), 4)
            results_df.append({
                'fk':
                kpi_ecaps_summary_fk,
                SessionResultsConsts.NUMERATOR_ID:
                self.own_manufacturer,
                SessionResultsConsts.DENOMINATOR_ID:
                self.store_fk,
                SessionResultsConsts.DENOMINATOR_RESULT:
                len(brands),
                SessionResultsConsts.NUMERATOR_RESULT:
                total_brand_score,
                SessionResultsConsts.RESULT:
                result_summary,
                SessionResultsConsts.SCORE:
                result_summary,
                'identifier_result':
                identifier_ecaps_summary
            })
        return results_df

    def gsk_pos_kpis(self):
        """
        Function calculate POSM Distribution
        :return
          - results :  array of dictionary, each dict contains kpi's result details
        """
        results = []
        OOS = 1
        DISTRIBUTED = 2

        self.gsk_generator.tool_box.extract_data_set_up_file(
            Consts.POSM, self.set_up_data, Consts.KPI_DICT)
        assortment_pos = self.msl_assortment(Consts.POSM_SKU, Consts.POSM)

        kpi_gsk_pos_distribution_store_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.GSK_POS_DISTRIBUTION_STORE)
        kpi_gsk_pos_distribution_brand_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.GSK_POS_DISTRIBUTION_BRAND)
        kpi_gsk_pos_distribution_sku_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.GSK_POS_DISTRIBUTION_SKU)

        if assortment_pos is None or assortment_pos.empty:
            Log.info(
                "Assortment df is empty. GSK_POS_DISTRIBUTION Kpis are not calculated"
            )
            return results

        # Calculate KPI : GSK_POS_DISTRIBUTION_STORE
        assortment_pos['in_store'] = assortment_pos['in_store'].astype('int')
        Log.info(
            "Dropping duplicate product_fks accros multiple-granular groups")
        Log.info("Before : {}".format(len(assortment_pos)))
        assortment_pos = assortment_pos.drop_duplicates(
            subset=[ProductsConsts.PRODUCT_FK])
        Log.info("After : {}".format(len(assortment_pos)))

        numerator_res = len(assortment_pos[assortment_pos['in_store'] == 1])
        denominator_res = len(assortment_pos)

        result = round(
            (numerator_res /
             float(denominator_res)), 4) if denominator_res != 0 else 0

        results.append({
            'fk': kpi_gsk_pos_distribution_store_fk,
            SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer,
            SessionResultsConsts.DENOMINATOR_ID: self.store_fk,
            SessionResultsConsts.NUMERATOR_RESULT: numerator_res,
            SessionResultsConsts.DENOMINATOR_RESULT: denominator_res,
            SessionResultsConsts.RESULT: result,
            SessionResultsConsts.SCORE: result,
            # 'identifier_parent': identifier_ecaps_summary,
            'identifier_result': "Gsk_Pos_Distribution_Store",
            'should_enter': True
        })

        # Calculate KPI: GSK_POS_DISTRIBUTION_BRAND
        brands_group = assortment_pos.groupby([ProductsConsts.BRAND_FK])
        for brand, assortment_pos_by_brand in brands_group:
            numerator_res = len(assortment_pos_by_brand[
                assortment_pos_by_brand['in_store'] == 1])
            denominator_res = len(assortment_pos_by_brand)
            result = round(
                (numerator_res /
                 float(denominator_res)), 4) if denominator_res != 0 else 0

            results.append({
                'fk':
                kpi_gsk_pos_distribution_brand_fk,
                SessionResultsConsts.NUMERATOR_ID:
                int(brand),
                SessionResultsConsts.DENOMINATOR_ID:
                self.store_fk,
                SessionResultsConsts.NUMERATOR_RESULT:
                numerator_res,
                SessionResultsConsts.DENOMINATOR_RESULT:
                denominator_res,
                SessionResultsConsts.RESULT:
                result,
                SessionResultsConsts.SCORE:
                result,
                'identifier_parent':
                "Gsk_Pos_Distribution_Store",
                'identifier_result':
                "Gsk_Pos_Distribution_Brand_" + str(int(brand)),
                'should_enter':
                True
            })

            for idx, each_product in assortment_pos_by_brand.iterrows():
                product_fk = each_product[ProductsConsts.PRODUCT_FK]
                result = 1 if int(each_product['in_store']) == 1 else 0
                result_status = DISTRIBUTED if result == 1 else OOS
                last_status = self.gsk_generator.tool_box.get_last_status(
                    kpi_gsk_pos_distribution_sku_fk, product_fk)

                results.append({
                    'fk':
                    kpi_gsk_pos_distribution_sku_fk,
                    SessionResultsConsts.NUMERATOR_ID:
                    product_fk,
                    SessionResultsConsts.DENOMINATOR_ID:
                    self.store_fk,
                    SessionResultsConsts.NUMERATOR_RESULT:
                    result,
                    SessionResultsConsts.DENOMINATOR_RESULT:
                    1,
                    SessionResultsConsts.RESULT:
                    result_status,
                    SessionResultsConsts.SCORE:
                    last_status,
                    'identifier_parent':
                    "Gsk_Pos_Distribution_Brand_" + str(int(brand)),
                    'identifier_result':
                    "Gsk_Pos_Distribution_SKU_" + str(int(product_fk)),
                    'should_enter':
                    True
                })

        return results
示例#3
0
class ToolBox(GlobalSessionToolBox):
    def __init__(self, data_provider, output):
        GlobalSessionToolBox.__init__(self, data_provider, output)
        self.adjacency = Adjancency(data_provider)
        self.block = Block(data_provider)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.ps_data_provider = PsDataProvider(data_provider)
        self._scene_types = None
        self.external_targets = self.ps_data_provider.get_kpi_external_targets(
        )

    @property
    def scene_types(self):
        if not self._scene_types:
            self._scene_types = self.scif['template_fk'].unique().tolist()
        return self._scene_types

    def main_calculation(self):
        custom_kpis = self.kpi_static_data[
            (self.kpi_static_data['kpi_calculation_stage_fk'] == 3)
            & (self.kpi_static_data['valid_from'] <= self.visit_date) &
            ((self.kpi_static_data['valid_until']).isnull() |
             (self.kpi_static_data['valid_until'] >= self.visit_date))]

        for kpi in custom_kpis.itertuples():
            kpi_function = self.get_kpi_function_by_family_fk(
                kpi.kpi_family_fk)
            kpi_function(kpi.pk)
        return

    @run_for_every_scene_type
    def calculate_presence(self, kpi_fk, template_fk=None):
        config = self.get_external_target_data_by_kpi_fk(kpi_fk)
        if config.empty or (template_fk is None):
            return

        result_df = self.scif[
            self.scif[config.numerator_param].isin(config.numerator_value)
            & (self.scif['template_fk'] == template_fk)]
        numerator_id = self.get_brand_fk_from_brand_name(
            config.numerator_value[0])
        result = 0 if result_df.empty else 1
        self.write_to_db(kpi_fk,
                         numerator_id=numerator_id,
                         denominator_id=template_fk,
                         result=result)
        return

    @run_for_every_scene_type
    def calculate_shelf_location(self, kpi_fk, template_fk=None):
        config = self.get_external_target_data_by_kpi_fk(kpi_fk)
        shelf_location = config.shelf_location
        if config.empty or (template_fk is None):
            return

        relevant_scene_fks = self.scif[
            self.scif['template_fk'] ==
            template_fk]['scene_fk'].unique().tolist()
        relevant_matches = self.matches[self.matches['scene_fk'].isin(
            relevant_scene_fks)]

        shelves = relevant_matches.groupby(
            'bay_number',
            as_index=False)['shelf_number'].max()['shelf_number'].mean()

        products_df = self.scif[
            (self.scif[config.numerator_param].isin(config.numerator_value))
            & (self.scif['template_fk'] == template_fk)]

        products_list = products_df['product_fk'].unique().tolist()

        if shelf_location == 'top':
            shelf_matches = relevant_matches[
                (relevant_matches['product_fk'].isin(products_list))
                & (relevant_matches['shelf_number'] <= (shelves / 3))]
        elif shelf_location == 'middle_bottom':
            shelf_matches = relevant_matches[
                (relevant_matches['product_fk'].isin(products_list))
                & (relevant_matches['shelf_number'] > (shelves / 3))]
        else:
            shelf_matches = pd.DataFrame()

        numerator_id = self.get_brand_fk_from_brand_name(
            config.numerator_value[0])
        result = 0 if shelf_matches.empty else 1
        self.write_to_db(kpi_fk,
                         numerator_id=numerator_id,
                         denominator_id=template_fk,
                         result=result)

    @run_for_every_scene_type
    def calculate_blocking(self, kpi_fk, template_fk=None):
        config = self.get_external_target_data_by_kpi_fk(kpi_fk)
        if config.empty or (template_fk is None):
            return
        location = {'template_fk': template_fk}
        blocks = self.block.network_x_block_together(
            {config.numerator_param: config.numerator_value},
            location,
            additional={'check_vertical_horizontal': True})
        if not blocks.empty:
            blocks = blocks[blocks['is_block']]
            orientation = config.orientation
            if orientation and orientation is not pd.np.nan:
                blocks = blocks[blocks['orientation'] == orientation]

        numerator_id = self.get_brand_fk_from_brand_name(
            config.numerator_value[0])
        result = 0 if blocks.empty else 1
        self.write_to_db(kpi_fk,
                         numerator_id=numerator_id,
                         denominator_id=template_fk,
                         result=result)

    @run_for_every_scene_type
    def calculate_adjacency(self, kpi_fk, template_fk=None):
        config = self.get_external_target_data_by_kpi_fk(kpi_fk)
        if config.empty or (template_fk is None):
            return
        location = {'template_fk': template_fk}
        anchor_pks = \
            self.scif[self.scif[config.anchor_param].isin(config.anchor_value)]['product_fk'].unique().tolist()
        tested_pks = \
            self.scif[self.scif[config.tested_param].isin(config.tested_value)]['product_fk'].unique().tolist()
        # handle populations that are not mutually exclusive
        tested_pks = [x for x in tested_pks if x not in anchor_pks]

        population = {
            'anchor_products': {
                'product_fk': anchor_pks
            },
            'tested_products': {
                'product_fk': tested_pks
            }
        }

        # this function is only needed until the adjacency function is enhanced to not crash when an empty population
        # is provided
        if self.check_population_exists(population, template_fk):
            try:
                adj_df = self.adjacency.network_x_adjacency_calculation(
                    population, location, {
                        'minimum_facings_adjacent': 1,
                        'minimum_block_ratio': 0,
                        'minimum_facing_for_block': 1,
                        'include_stacking': True
                    })
            except AttributeError:
                Log.info(
                    "Error calculating adjacency for kpi_fk {} template_fk {}".
                    format(kpi_fk, template_fk))
                return
            if adj_df.empty:
                result = 0
            else:
                result = 1 if not adj_df[adj_df['is_adj']].empty else 0
        else:
            result = 0
        numerator_id = self.get_brand_fk_from_brand_name(
            config.anchor_value[0])
        self.write_to_db(kpi_fk,
                         numerator_id=numerator_id,
                         denominator_id=template_fk,
                         result=result)
        return

    @run_for_every_scene_type
    def calculate_brand_facings(self, kpi_fk, template_fk=None):
        relevant_scif = self.scif[self.scif['template_fk'] == template_fk]

        denominator_results = relevant_scif.groupby(
            'Customer Category', as_index=False)[[
                'facings'
            ]].sum().rename(columns={'facings': 'denominator_result'})

        numerator_result = relevant_scif.groupby(
            ['brand_fk', 'Customer Category'], as_index=False)[[
                'facings'
            ]].sum().rename(columns={'facings': 'numerator_result'})

        results = numerator_result.merge(denominator_results)
        results['result'] = (results['numerator_result'] /
                             results['denominator_result'])
        results['result'].fillna(0, inplace=True)

        for index, row in results.iterrows():
            relevant_perfetti_product_fk = self.get_product_fk_from_perfetti_category(
                row['Customer Category'])
            self.write_to_db(fk=kpi_fk,
                             numerator_id=row['brand_fk'],
                             denominator_id=relevant_perfetti_product_fk,
                             numerator_result=row['numerator_result'],
                             denominator_result=row['denominator_result'],
                             context_id=template_fk,
                             result=row['result'],
                             score=row['result'])

    def get_kpi_function_by_family_fk(self, kpi_family_fk):
        if kpi_family_fk == 19:
            return self.calculate_presence
        elif kpi_family_fk == 20:
            return self.calculate_adjacency
        elif kpi_family_fk == 21:
            return self.calculate_blocking
        elif kpi_family_fk == 22:
            return self.calculate_shelf_location
        elif kpi_family_fk == 23:
            return self.calculate_brand_facings

    def get_external_target_data_by_kpi_fk(self, kpi_fk):
        return self.external_targets[self.external_targets['kpi_fk'] ==
                                     kpi_fk].iloc[0]

    def get_brand_fk_from_brand_name(self, brand_name):
        return self.all_products[self.all_products['brand_name'] ==
                                 brand_name]['brand_fk'].iloc[0]

    def get_product_fk_from_perfetti_category(self, perfetti_category):
        try:
            return self.all_products[self.all_products['Customer Category'] ==
                                     perfetti_category]['product_fk'].iloc[0]
        except IndexError:
            return None

    def check_population_exists(self, population, template_fk):
        relevant_scif = self.scif[self.scif['template_fk'] == template_fk]
        anchor_scif = relevant_scif[relevant_scif['product_fk'].isin(
            population['anchor_products']['product_fk'])]
        tested_scif = relevant_scif[relevant_scif['product_fk'].isin(
            population['tested_products']['product_fk'])]
        if anchor_scif.empty or tested_scif.empty:
            return False
        else:
            return True
class ALTRIAUSToolBox:
    LEVEL1 = 1
    LEVEL2 = 2
    LEVEL3 = 3

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = Common(self.data_provider)
        self.common_v2 = CommonV2(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.template_info = self.data_provider.all_templates
        self.rds_conn = ProjectConnector(self.project_name,
                                         DbUsers.CalculationEng)
        self.ps_data_provider = PsDataProvider(self.data_provider)
        self.match_product_in_probe_state_reporting = self.ps_data_provider.get_match_product_in_probe_state_reporting(
        )
        self.thresholds_and_results = {}
        self.result_df = []
        self.writing_to_db_time = datetime.timedelta(0)
        self.kpi_results_queries = []
        self.potential_products = {}
        self.shelf_square_boundaries = {}
        self.average_shelf_values = {}
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.kpi_results_queries = []
        self.all_template_data = parse_template(TEMPLATE_PATH, "KPI")
        self.spacing_template_data = parse_template(TEMPLATE_PATH, "Spacing")
        self.empty_data_template = parse_template(TEMPLATE_PATH, "Empty")
        self.fixture_width_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE,
                                                    "Fixture Width",
                                                    dtype=pd.Int64Dtype())
        self.facings_to_feet_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE,
                                                      "Conversion Table",
                                                      dtype=pd.Int64Dtype())
        self.header_positions_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE,
                                                       "Header Positions")
        self.flip_sign_positions_template = pd.read_excel(
            FIXTURE_WIDTH_TEMPLATE, "Flip Sign Positions")
        self.custom_entity_data = self.ps_data_provider.get_custom_entities(
            1005)
        self.ignore_stacking = False
        self.facings_field = 'facings' if not self.ignore_stacking else 'facings_ign_stack'
        self.INCLUDE_FILTER = 1
        self.assortment = Assortment(self.data_provider,
                                     output=self.output,
                                     ps_data_provider=self.ps_data_provider)
        self.store_assortment = self.assortment.get_lvl3_relevant_ass()

        self.kpi_new_static_data = self.common.get_new_kpi_static_data()
        try:
            self.mpis = self.match_product_in_scene.merge(self.products, on='product_fk', suffixes=['', '_p']) \
                        .merge(self.scene_info, on='scene_fk', suffixes=['', '_s']) \
                          .merge(self.template_info, on='template_fk', suffixes=['', '_t'])
        except KeyError:
            Log.warning('MPIS cannot be generated!')
            return
        self.adp = AltriaDataProvider(self.data_provider)
        self.active_kpis = self._get_active_kpis()
        self.external_targets = self.ps_data_provider.get_kpi_external_targets(
        )
        self.survey_dot_com_collected_this_session = self._get_survey_dot_com_collected_value(
        )

    def main_calculation(self, *args, **kwargs):
        """
               This function calculates the KPI results.
               """

        self.calculate_signage_locations_and_widths('Cigarettes')
        self.calculate_signage_locations_and_widths('Smokeless')
        self.calculate_register_type()
        self.calculate_age_verification()
        self.calculate_juul_availability()
        self.calculate_assortment()
        self.calculate_vapor_kpis()
        self.calculate_empty_brand()
        self.calculate_facings_by_scene_type()

        kpi_set_fk = 2
        set_name = \
            self.kpi_static_data.loc[self.kpi_static_data['kpi_set_fk'] == kpi_set_fk]['kpi_set_name'].values[0]
        template_data = self.all_template_data.loc[
            self.all_template_data['KPI Level 1 Name'] == set_name]

        try:
            if set_name and not set(
                    template_data['Scene Types to Include'].values[0].encode(
                    ).split(', ')) & set(
                        self.scif['template_name'].unique().tolist()):
                Log.info('Category {} was not captured'.format(
                    template_data['category'].values[0]))
                return
        except Exception as e:
            Log.info(
                'KPI Set {} is not defined in the template'.format(set_name))

        for i, row in template_data.iterrows():
            try:
                kpi_name = row['KPI Level 2 Name']
                if kpi_name in KPI_LEVEL_2_cat_space:
                    # scene_type = [s for s in row['Scene_Type'].encode().split(', ')]
                    kpi_type = row['KPI Type']
                    scene_type = row['scene_type']

                    if row['Param1'] == 'Category' or 'sub_category':
                        category = row['Value1']

                        if kpi_type == 'category_space':
                            kpi_set_fk = \
                            self.kpi_new_static_data.loc[self.kpi_new_static_data['type'] == kpi_type]['pk'].values[0]
                            self.calculate_category_space(
                                kpi_set_fk,
                                kpi_name,
                                category,
                                scene_types=scene_type)

            except Exception as e:
                Log.info('KPI {} calculation failed due to {}'.format(
                    kpi_name.encode('utf-8'), e))
                continue
        return

    def _get_survey_dot_com_collected_value(self):
        try:
            sales_rep_fk = self.session_info['s_sales_rep_fk'].iloc[0]
        except IndexError:
            sales_rep_fk = 0

        return int(sales_rep_fk) == 209050

    def _get_active_kpis(self):
        active_kpis = self.kpi_new_static_data[
            (self.kpi_new_static_data['kpi_calculation_stage_fk'] == 3)
            & (self.kpi_new_static_data['valid_from'] <= self.visit_date) &
            ((self.kpi_new_static_data['valid_until']).isnull() |
             (self.kpi_new_static_data['valid_until'] >= self.visit_date))]
        return active_kpis

    def calculate_vapor_kpis(self):
        category = 'Vapor'
        relevant_scif = self.scif[self.scif['template_name'] ==
                                  'JUUL Merchandising']
        if relevant_scif.empty:
            Log.info('No products found for {} category'.format(category))
            return

        relevant_scif = relevant_scif[
            (relevant_scif['category'].isin([category, 'POS']))
            & (relevant_scif['brand_name'] == 'Juul')]
        if relevant_scif.empty:
            return
        relevant_product_pks = relevant_scif[
            relevant_scif['product_type'] ==
            'SKU']['product_fk'].unique().tolist()
        relevant_scene_id = self.get_most_frequent_scene(relevant_scif)
        product_mpis = self.mpis[
            (self.mpis['product_fk'].isin(relevant_product_pks))
            & (self.mpis['scene_fk'] == relevant_scene_id)]

        if product_mpis.empty:
            Log.info('No products found for {} category'.format(category))
            return

        self.calculate_total_shelves(product_mpis, category, product_mpis)

        longest_shelf = \
            product_mpis[product_mpis['shelf_number'] ==
                         self.get_longest_shelf_number(product_mpis,
                                                       max_shelves_from_top=999)].sort_values(by='rect_x',
                                                                                              ascending=True)

        if longest_shelf.empty or longest_shelf.isnull().all().all():
            Log.warning(
                'The {} category items are in a non-standard location. The {} category will not be calculated.'
                .format(category, category))
            return

        relevant_pos = pd.DataFrame()
        self.calculate_fixture_width(relevant_pos, longest_shelf, category)
        return

    def calculate_assortment(self):
        if self.scif.empty or self.store_assortment.empty:
            Log.warning(
                'Unable to calculate assortment: SCIF or store assortment is empty'
            )
            return

        grouped_scif = self.scif.groupby('product_fk',
                                         as_index=False)['facings'].sum()
        assortment_with_facings = \
            pd.merge(self.store_assortment, grouped_scif, how='left', on='product_fk')
        assortment_with_facings.loc[:, 'facings'] = assortment_with_facings[
            'facings'].fillna(0)

        for product in assortment_with_facings.itertuples():
            score = 1 if product.facings > 0 else 0
            self.common_v2.write_to_db_result(
                product.kpi_fk_lvl3,
                numerator_id=product.product_fk,
                denominator_id=product.assortment_fk,
                numerator_result=product.facings,
                result=product.facings,
                score=score)

        number_of_skus_present = len(
            assortment_with_facings[assortment_with_facings['facings'] > 0])
        score = 1 if number_of_skus_present > 0 else 0
        kpi_fk = assortment_with_facings['kpi_fk_lvl2'].iloc[0]
        assortment_group_fk = assortment_with_facings[
            'assortment_group_fk'].iloc[0]
        self.common_v2.write_to_db_result(
            kpi_fk,
            numerator_id=assortment_group_fk,
            numerator_result=number_of_skus_present,
            denominator_result=len(assortment_with_facings),
            result=number_of_skus_present,
            score=score)

    def calculate_facings_by_scene_type(self):
        kpi_name = 'FACINGS_BY_SCENE_TYPE'
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(kpi_name)
        if kpi_name not in self.active_kpis['type'].unique().tolist():
            return

        config = self.get_external_target_data_by_kpi_fk(kpi_fk)

        product_types = config.product_type
        template_names = config.template_name

        relevant_mpis = self.mpis[
            (self.mpis['product_type'].isin(product_types))
            & (self.mpis['template_name'].isin(template_names))]
        smart_attribute_data = \
            self.adp.get_match_product_in_probe_state_values(relevant_mpis['probe_match_fk'].unique().tolist())

        relevant_mpis = pd.merge(relevant_mpis,
                                 smart_attribute_data,
                                 on='probe_match_fk',
                                 how='left')
        relevant_mpis['match_product_in_probe_state_fk'].fillna(0,
                                                                inplace=True)

        relevant_mpis = relevant_mpis.groupby(
            ['product_fk', 'template_fk', 'match_product_in_probe_state_fk'],
            as_index=False)['scene_match_fk'].count()
        relevant_mpis.rename(columns={'scene_match_fk': 'facings'},
                             inplace=True)

        for row in relevant_mpis.itertuples():
            self.common_v2.write_to_db_result(
                kpi_fk,
                numerator_id=row.product_fk,
                denominator_id=row.template_fk,
                context_id=row.match_product_in_probe_state_fk,
                result=row.facings)

        return

    def calculate_register_type(self):
        if self.survey_dot_com_collected_this_session:
            return
        relevant_scif = self.scif[
            (self.scif['product_type'].isin(['POS', 'Other']))
            & (self.scif['category'] == 'POS Machinery')]
        if relevant_scif.empty:
            result = 0
            product_fk = 0
        else:
            result = 1
            product_fk = relevant_scif['product_fk'].iloc[0]

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Register Type')
        self.common_v2.write_to_db_result(kpi_fk,
                                          numerator_id=product_fk,
                                          denominator_id=self.store_id,
                                          result=result)

    def calculate_age_verification(self):
        if self.survey_dot_com_collected_this_session:
            return
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Age Verification')
        relevant_scif = self.scif[self.scif['brand_name'].isin(
            ['Age Verification'])]

        if relevant_scif.empty:
            result = 0
            product_fk = 0

            self.common_v2.write_to_db_result(kpi_fk,
                                              numerator_id=product_fk,
                                              denominator_id=self.store_id,
                                              result=result)
        else:
            result = 1
            for product_fk in relevant_scif['product_fk'].unique().tolist():
                self.common_v2.write_to_db_result(kpi_fk,
                                                  numerator_id=product_fk,
                                                  denominator_id=self.store_id,
                                                  result=result)
        return

    def calculate_empty_brand(self):
        product_type = ['Empty']
        for i, row in self.empty_data_template.iterrows():

            kpi_name = row['KPI Level 2 Name']
            param1 = row['Param1']
            value1 = row['Value1']
            kpi_fk = self.common_v2.get_kpi_fk_by_kpi_name(kpi_name)

            if value1.find(',') != -1:
                value1 = value1.split(',')
            for value in value1:
                relevant_scif = self.scif[(self.scif[param1] == value) & (
                    self.scif['product_type'].isin(product_type))]
                if relevant_scif.empty:
                    pass
                else:
                    result = empty_facings = relevant_scif['facings'].sum()
                    brand_fk = relevant_scif['brand_fk'].iloc[0]

                    self.common_v2.write_to_db_result(
                        kpi_fk,
                        numerator_id=brand_fk,
                        numerator_result=empty_facings,
                        denominator_id=self.store_id,
                        result=result,
                        score=0)

    def calculate_juul_availability(self):
        relevant_scif = self.scif[(self.scif['brand_name'].isin(['Juul']))
                                  & (self.scif['product_type'].isin(['POS']))]
        juul_pos = relevant_scif['product_fk'].unique().tolist()

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Juul POS Availability')
        if not juul_pos:
            return

        result = 1
        for product_fk in juul_pos:
            self.common_v2.write_to_db_result(kpi_fk,
                                              numerator_id=product_fk,
                                              denominator_id=self.store_id,
                                              result=result)

    def calculate_category_space(self,
                                 kpi_set_fk,
                                 kpi_name,
                                 category,
                                 scene_types=None):
        template = self.all_template_data.loc[
            (self.all_template_data['KPI Level 2 Name'] == kpi_name)
            & (self.all_template_data['Value1'] == category)]
        kpi_template = template.loc[template['KPI Level 2 Name'] == kpi_name]
        if kpi_template.empty:
            return None
        kpi_template = kpi_template.iloc[0]
        values_to_check = []

        filters = {
            'template_name': scene_types,
            'category': kpi_template['Value1']
        }

        if kpi_template['Value1'] in CATEGORIES:
            category_att = 'category'

        if kpi_template['Value1']:
            values_to_check = self.all_products.loc[
                self.all_products[category_att] ==
                kpi_template['Value1']][category_att].unique().tolist()

        for primary_filter in values_to_check:
            filters[kpi_template['Param1']] = primary_filter

            new_kpi_name = self.kpi_name_builder(kpi_name, **filters)

            result = self.calculate_category_space_length(
                new_kpi_name, **filters)
            filters['Category'] = kpi_template['KPI Level 2 Name']
            score = result
            numerator_id = self.products['category_fk'][
                self.products['category'] == kpi_template['Value1']].iloc[0]
            self.common_v2.write_to_db_result(kpi_set_fk,
                                              numerator_id=numerator_id,
                                              numerator_result=999,
                                              result=score,
                                              score=score)

    def calculate_category_space_length(self,
                                        kpi_name,
                                        threshold=0.5,
                                        retailer=None,
                                        exclude_pl=False,
                                        **filters):
        """
        :param threshold: The ratio for a bay to be counted as part of a category.
        :param filters: These are the parameters which the data frame is filtered by.
        :return: The total shelf width (in mm) the relevant facings occupy.
        """

        try:
            filtered_scif = self.scif[self.get_filter_condition(
                self.scif, **filters)]
            space_length = 0
            bay_values = []
            for scene in filtered_scif['scene_fk'].unique().tolist():
                scene_matches = self.mpis[self.mpis['scene_fk'] == scene]
                scene_filters = filters
                scene_filters['scene_fk'] = scene
                for bay in scene_matches['bay_number'].unique().tolist():
                    bay_total_linear = scene_matches.loc[
                        (scene_matches['bay_number'] == bay)
                        & (scene_matches['stacking_layer'] == 1) &
                        (scene_matches['status']
                         == 1)]['width_mm_advance'].sum()
                    scene_filters['bay_number'] = bay

                    tested_group_linear = scene_matches[
                        self.get_filter_condition(scene_matches,
                                                  **scene_filters)]

                    tested_group_linear_value = tested_group_linear[
                        'width_mm_advance'].sum()

                    if tested_group_linear_value:
                        bay_ratio = tested_group_linear_value / float(
                            bay_total_linear)
                    else:
                        bay_ratio = 0

                    if bay_ratio >= threshold:
                        category = filters['category']
                        max_facing = scene_matches.loc[
                            (scene_matches['bay_number'] == bay)
                            & (scene_matches['stacking_layer'] == 1
                               )]['facing_sequence_number'].max()
                        shelf_length = self.spacing_template_data.query(
                            'Category == "' + category + '" & Low <= "' +
                            str(max_facing) + '" & High >= "' +
                            str(max_facing) + '"')
                        shelf_length = int(shelf_length['Size'].iloc[-1])
                        bay_values.append(shelf_length)
                        space_length += shelf_length
        except Exception as e:
            Log.info('Linear Feet calculation failed due to {}'.format(e))
            space_length = 0

        return space_length

    def get_filter_condition(self, df, **filters):
        """
        :param df: The data frame to be filters.
        :param filters: These are the parameters which the data frame is filtered by.
                       Every parameter would be a tuple of the value and an include/exclude flag.
                       INPUT EXAMPLE (1):   manufacturer_name = ('Diageo', DIAGEOAUPNGROGENERALToolBox.INCLUDE_FILTER)
                       INPUT EXAMPLE (2):   manufacturer_name = 'Diageo'
        :return: a filtered Scene Item Facts data frame.
        """
        if not filters:
            return df['pk'].apply(bool)
        if self.facings_field in df.keys():
            filter_condition = (df[self.facings_field] > 0)
        else:
            filter_condition = None
        for field in filters.keys():
            if field in df.keys():
                if isinstance(filters[field], tuple):
                    value, exclude_or_include = filters[field]
                else:
                    value, exclude_or_include = filters[
                        field], self.INCLUDE_FILTER
                if not value:
                    continue
                if not isinstance(value, list):
                    value = [value]
                if exclude_or_include == self.INCLUDE_FILTER:
                    condition = (df[field].isin(value))
                elif exclude_or_include == self.EXCLUDE_FILTER:
                    condition = (~df[field].isin(value))
                elif exclude_or_include == self.CONTAIN_FILTER:
                    condition = (df[field].str.contains(value[0], regex=False))
                    for v in value[1:]:
                        condition |= df[field].str.contains(v, regex=False)
                else:
                    continue
                if filter_condition is None:
                    filter_condition = condition
                else:
                    filter_condition &= condition
            else:
                Log.warning('field {} is not in the Data Frame'.format(field))

        return filter_condition

    def kpi_name_builder(self, kpi_name, **filters):
        """
        This function builds kpi name according to naming convention
        """
        for filter in filters.keys():
            if filter == 'template_name':
                continue
            kpi_name = kpi_name.replace('{' + filter + '}',
                                        str(filters[filter]))
            kpi_name = kpi_name.replace("'", "\'")
        return kpi_name

    def calculate_signage_locations_and_widths(self, category):
        excluded_types = ['Other', 'Irrelevant', 'Empty']
        relevant_scif = self.scif[self.scif['template_name'] ==
                                  'Tobacco Merchandising Space']
        if relevant_scif.empty:
            Log.info('No products found for {} category'.format(category))
            return
        # need to include scene_id from previous relevant_scif
        # also need to split this shit up into different categories, i.e. smokeless, cigarettes
        # need to figure out how to deal with POS from smokeless being included with cigarette MPIS

        # get relevant SKUs from the cigarettes category
        relevant_scif = relevant_scif[relevant_scif['category'].isin(
            [category, 'POS'])]
        relevant_product_pks = relevant_scif[
            relevant_scif['product_type'] ==
            'SKU']['product_fk'].unique().tolist()
        relevant_pos_pks = \
            relevant_scif[(relevant_scif['product_type'] == 'POS') &
                          ~(relevant_scif['brand_name'] == 'Age Verification') &
                          ~(relevant_scif['product_name'] == 'General POS Other')]['product_fk'].unique().tolist()
        other_product_and_pos_pks = \
            relevant_scif[relevant_scif['product_type'].isin(excluded_types)]['product_fk'].tolist()
        relevant_scene_id = self.get_most_frequent_scene(relevant_scif)
        product_mpis = self.mpis[
            (self.mpis['product_fk'].isin(relevant_product_pks))
            & (self.mpis['scene_fk'] == relevant_scene_id)]

        if product_mpis.empty:
            Log.info('No products found for {} category'.format(category))
            return

        self.calculate_total_shelves(product_mpis, category)

        longest_shelf = \
            product_mpis[product_mpis['shelf_number'] ==
                         self.get_longest_shelf_number(product_mpis)].sort_values(by='rect_x', ascending=True)

        if longest_shelf.empty or longest_shelf.isnull().all().all():
            Log.warning(
                'The {} category items are in a non-standard location. The {} category will not be calculated.'
                .format(category, category))
            return

        # demarcation_line = longest_shelf['rect_y'].median() old method, had bugs due to longest shelf being lower
        demarcation_line = product_mpis['rect_y'].min()

        exclusion_line = -9999
        excluded_mpis = self.mpis[
            ~(self.mpis['product_fk'].isin(relevant_pos_pks +
                                           relevant_product_pks +
                                           other_product_and_pos_pks))
            & (self.mpis['rect_x'] < longest_shelf['rect_x'].max()) &
            (self.mpis['rect_x'] > longest_shelf['rect_x'].min()) &
            (self.mpis['scene_fk'] == relevant_scene_id) &
            (self.mpis['rect_y'] < demarcation_line)]
        # we need this line for when SCIF and MPIS don't match
        excluded_mpis = excluded_mpis[~excluded_mpis['product_type'].
                                      isin(excluded_types)]

        if not excluded_mpis.empty:
            exclusion_line = excluded_mpis['rect_y'].max()

        # we need to get POS stuff that falls within the x-range of the longest shelf (which is limited by category)
        # we also need to account for the fact that the images suck, so we're going to add/subtract 5% of the
        # max/min values to allow for POS items that fall slightly out of the shelf length range
        correction_factor = 0.05
        correction_value = (longest_shelf['rect_x'].max() -
                            longest_shelf['rect_x'].min()) * correction_factor
        pos_mpis = self.mpis[
            (self.mpis['product_fk'].isin(relevant_pos_pks))
            & (self.mpis['rect_x'] <
               (longest_shelf['rect_x'].max() + correction_value)) &
            (self.mpis['rect_x'] >
             (longest_shelf['rect_x'].min() - correction_value)) &
            (self.mpis['scene_fk'] == relevant_scene_id)]

        # DO NOT SET TO TRUE WHEN DEPLOYING
        # debug flag displays polygon_mask graph DO NOT SET TO TRUE WHEN DEPLOYING
        # DO NOT SET TO TRUE WHEN DEPLOYING
        relevant_pos = self.adp.get_products_contained_in_displays(
            pos_mpis, y_axis_threshold=35, debug=False)

        if relevant_pos.empty:
            Log.warning(
                'No polygon mask was generated for {} category - cannot compute KPIs'
                .format(category))
            # we need to attempt to calculate fixture width, even if there's no polygon mask
            self.calculate_fixture_width(relevant_pos, longest_shelf, category)
            return

        relevant_pos = relevant_pos[[
            'product_fk', 'product_name', 'left_bound', 'right_bound',
            'center_x', 'center_y'
        ]]
        relevant_pos = relevant_pos.reindex(
            columns=relevant_pos.columns.tolist() +
            ['type', 'width', 'position'])
        relevant_pos['width'] = \
            relevant_pos.apply(lambda row: self.get_length_of_pos(row, longest_shelf, category), axis=1)
        relevant_pos['type'] = \
            relevant_pos['center_y'].apply(lambda x: 'Header' if exclusion_line < x < demarcation_line else 'Flip Sign')
        relevant_pos = relevant_pos.sort_values(['center_x'], ascending=True)
        relevant_pos = self.remove_duplicate_pos_tags(relevant_pos)
        # generate header positions
        if category == 'Cigarettes':
            number_of_headers = len(
                relevant_pos[relevant_pos['type'] == 'Header'])
            if number_of_headers > len(
                    self.header_positions_template['Cigarettes Positions'].
                    dropna()):
                Log.warning(
                    'Number of Headers for Cigarettes is greater than max number defined in template!'
                )
            elif number_of_headers > 0:
                header_position_list = [
                    position.strip()
                    for position in self.header_positions_template[
                        self.header_positions_template['Number of Headers'] ==
                        number_of_headers]
                    ['Cigarettes Positions'].iloc[0].split(',')
                ]
                relevant_pos.loc[relevant_pos['type'] == 'Header',
                                 ['position']] = header_position_list
        elif category == 'Smokeless':
            relevant_pos = self.check_menu_scene_recognition(relevant_pos)
            number_of_headers = len(
                relevant_pos[relevant_pos['type'] == 'Header'])
            if number_of_headers > len(
                    self.header_positions_template['Smokeless Positions'].
                    dropna()):
                Log.warning(
                    'Number of Headers for Smokeless is greater than max number defined in template!'
                )
            elif number_of_headers > 0:
                header_position_list = [
                    position.strip()
                    for position in self.header_positions_template[
                        self.header_positions_template['Number of Headers'] ==
                        number_of_headers]
                    ['Smokeless Positions'].iloc[0].split(',')
                ]
                relevant_pos.loc[relevant_pos['type'] == 'Header',
                                 ['position']] = header_position_list

            relevant_pos = self.get_menu_board_items(relevant_pos,
                                                     longest_shelf, pos_mpis)
        # generate flip-sign positions
        if category == 'Cigarettes':
            relevant_template = \
                self.fixture_width_template.loc[(self.fixture_width_template['Fixture Width (facings)']
                                                 - len(longest_shelf)).abs().argsort()[:1]].dropna(axis=1)
            locations = relevant_template.columns[2:].tolist()
            right_bound = 0
            longest_shelf_copy = longest_shelf.copy()
            for location in locations:
                if right_bound > 0:
                    left_bound = right_bound + 1
                else:
                    left_bound = longest_shelf_copy.iloc[:relevant_template[
                        location].iloc[0]]['rect_x'].min()
                right_bound = longest_shelf_copy.iloc[:relevant_template[
                    location].iloc[0]]['rect_x'].max()
                if locations[-1] == location:
                    right_bound = right_bound + abs(right_bound * 0.05)
                flip_sign_pos = relevant_pos[
                    (relevant_pos['type'] == 'Flip Sign')
                    & (relevant_pos['center_x'] > left_bound) &
                    (relevant_pos['center_x'] < right_bound)]
                if flip_sign_pos.empty:
                    # add 'NO Flip Sign' product_fk
                    relevant_pos.loc[len(relevant_pos), ['position', 'product_fk', 'type']] = \
                        [location, NO_FLIP_SIGN_PK, 'Flip Sign']
                else:
                    relevant_pos.loc[flip_sign_pos.index,
                                     ['position']] = location
                longest_shelf_copy.drop(
                    longest_shelf_copy.iloc[:relevant_template[location].
                                            iloc[0]].index,
                    inplace=True)
        elif category == 'Smokeless':
            # if there are no flip signs found, there are no positions to assign
            number_of_flip_signs = len(
                relevant_pos[relevant_pos['type'] == 'Flip Sign'])
            if number_of_flip_signs > self.flip_sign_positions_template[
                    'Number of Flip Signs'].max():
                Log.warning(
                    'Number of Flip Signs for Smokeless is greater than max number defined in template!'
                )
            elif number_of_flip_signs > 0:
                flip_sign_position_list = [
                    position.strip()
                    for position in self.flip_sign_positions_template[
                        self.
                        flip_sign_positions_template['Number of Flip Signs'] ==
                        number_of_flip_signs]['Position'].iloc[0].split(',')
                ]
                relevant_pos.loc[relevant_pos['type'] == 'Flip Sign',
                                 ['position']] = flip_sign_position_list

            # store empty flip sign values
            for location in ['Secondary', 'Tertiary']:
                if location not in relevant_pos[
                        relevant_pos['type'] ==
                        'Flip Sign']['position'].tolist():
                    relevant_pos.loc[len(relevant_pos), ['position', 'product_fk', 'type']] = \
                        [location, NO_FLIP_SIGN_PK, 'Flip Sign']

        relevant_pos = relevant_pos.reindex(
            columns=relevant_pos.columns.tolist() + ['denominator_id'])

        # this is a bandaid fix that should be removed ->  'F7011A7C-1BB6-4007-826D-2B674BD99DAE'
        # removes POS items that were 'extra', i.e. more than max value in template
        # only affects smokeless
        relevant_pos.dropna(subset=['position'], inplace=True)

        relevant_pos.loc[:,
                         ['denominator_id']] = relevant_pos['position'].apply(
                             self.get_custom_entity_pk)

        for row in relevant_pos.itertuples():
            kpi_fk = self.common_v2.get_kpi_fk_by_kpi_name(row.type)
            self.common_v2.write_to_db_result(
                kpi_fk,
                numerator_id=row.product_fk,
                denominator_id=row.denominator_id,
                result=row.width,
                score=row.width)

        self.calculate_fixture_width(relevant_pos, longest_shelf, category)
        return

    def calculate_total_shelves(self,
                                longest_shelf,
                                category,
                                product_mpis=None):
        category_fk = self.get_category_fk_by_name(category)
        if product_mpis is None:
            product_mpis = self.mpis[
                (self.mpis['rect_x'] > longest_shelf['rect_x'].min())
                & (self.mpis['rect_x'] < longest_shelf['rect_x'].max()) &
                (self.mpis['scene_fk']
                 == longest_shelf['scene_fk'].fillna(0).mode().iloc[0])]
        total_shelves = len(product_mpis['shelf_number'].unique())

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_name('Total Shelves')
        self.common_v2.write_to_db_result(kpi_fk,
                                          numerator_id=category_fk,
                                          denominator_id=self.store_id,
                                          result=total_shelves)

    def calculate_fixture_width(self, relevant_pos, longest_shelf, category):
        longest_shelf = longest_shelf[longest_shelf['stacking_layer'] == 1]
        category_fk = self.get_category_fk_by_name(category)
        # ======== old method for synchronizing fixture width to header data ========
        # this is needed to remove intentionally duplicated 'Menu Board' POS 'Headers'
        # relevant_pos = relevant_pos.drop_duplicates(subset=['position'])
        # try:
        #     width = relevant_pos[relevant_pos['type'] == 'Header']['width'].sum()
        # except KeyError:
        #     # needed for when 'width' doesn't exist
        #     width = 0
        #
        # if relevant_pos.empty or width == 0:
        #     correction_factor = 1 if category == 'Smokeless' else 2
        #     width = round(len(longest_shelf) + correction_factor / float(
        #         self.facings_to_feet_template[category + ' Facings'].iloc[0]))
        # ======== old method for synchronizing fixture width to header data ========

        width = len(longest_shelf) / float(
            self.facings_to_feet_template[category + ' Facings'].iloc[0])

        self.mark_tags_in_explorer(longest_shelf['probe_match_fk'].tolist(),
                                   'Longest Shelf - ' + category)

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_name('Fixture Width')
        self.common_v2.write_to_db_result(kpi_fk,
                                          numerator_id=category_fk,
                                          denominator_id=self.store_id,
                                          result=width)

    def mark_tags_in_explorer(self, probe_match_fk_list, mpipsr_name):
        if not probe_match_fk_list:
            return
        try:
            match_type_fk = \
                self.match_product_in_probe_state_reporting[
                    self.match_product_in_probe_state_reporting['name'] == mpipsr_name][
                    'match_product_in_probe_state_reporting_fk'].values[0]
        except IndexError:
            Log.warning(
                'Name not found in match_product_in_probe_state_reporting table: {}'
                .format(mpipsr_name))
            return

        match_product_in_probe_state_values_old = self.common_v2.match_product_in_probe_state_values
        match_product_in_probe_state_values_new = pd.DataFrame(columns=[
            MATCH_PRODUCT_IN_PROBE_FK,
            MATCH_PRODUCT_IN_PROBE_STATE_REPORTING_FK
        ])
        match_product_in_probe_state_values_new[
            MATCH_PRODUCT_IN_PROBE_FK] = probe_match_fk_list
        match_product_in_probe_state_values_new[
            MATCH_PRODUCT_IN_PROBE_STATE_REPORTING_FK] = match_type_fk

        self.common_v2.match_product_in_probe_state_values = pd.concat([
            match_product_in_probe_state_values_old,
            match_product_in_probe_state_values_new
        ])

        return

    def get_category_fk_by_name(self, category_name):
        return self.all_products[self.all_products['category'] ==
                                 category_name]['category_fk'].iloc[0]

    def check_menu_scene_recognition(self, relevant_pos):
        mdis = self.adp.get_match_display_in_scene()
        mdis = mdis[mdis['display_name'] == 'Menu POS']

        if mdis.empty:
            return relevant_pos

        dummy_sku_for_menu_pk = 9282  # 'Other (Smokeless Tobacco)'
        dummy_sky_for_menu_name = 'Menu POS (Scene Recognized Item)'
        location_type = 'Header'
        width = 1
        center_x = mdis['x'].iloc[0]
        center_y = mdis['y'].iloc[0]
        relevant_pos.loc[len(relevant_pos), ['product_fk', 'product_name', 'center_x', 'center_y', 'type', 'width']] = \
            [dummy_sku_for_menu_pk, dummy_sky_for_menu_name, center_x, center_y, location_type, width]
        relevant_pos = relevant_pos.sort_values(
            ['center_x'], ascending=True).reset_index(drop=True)
        return relevant_pos

    def get_menu_board_items(self, relevant_pos, longest_shelf, pos_mpis):
        # get the placeholder item
        menu_board_dummy = relevant_pos[relevant_pos['product_name'] ==
                                        'Menu POS (Scene Recognized Item)']

        # if no placeholder, this function isn't relevant
        if menu_board_dummy.empty:
            return relevant_pos

        center_x = menu_board_dummy['center_x'].iloc[0]
        center_y = menu_board_dummy['center_y'].iloc[0]
        position = menu_board_dummy['position'].iloc[0]

        demarcation_line = longest_shelf['rect_y'].min()
        upper_demarcation_line = center_y - (demarcation_line - center_y)

        distance_in_facings = 2

        try:
            left_bound = longest_shelf[
                longest_shelf['rect_x'] < center_x].sort_values(
                    by=['rect_x'],
                    ascending=False)['rect_x'].iloc[int(distance_in_facings) -
                                                    1]
        except IndexError:
            # if there are no POS items found to the left of the 'Menu POS' scene recognition tag, use the tag itself
            # in theory this should never happen
            left_bound = center_x

        try:
            right_bound = longest_shelf[
                longest_shelf['rect_x'] > center_x].sort_values(
                    by=['rect_x'],
                    ascending=True)['rect_x'].iloc[int(distance_in_facings) -
                                                   1]
        except IndexError:
            # if there are no POS items found to the right of the 'Menu POS' scene recognition tag, use the tag itself
            # this is more likely to happen for the right bound than the left bound
            right_bound = center_x

        pos_mpis = pos_mpis[(pos_mpis['rect_x'] > left_bound)
                            & (pos_mpis['rect_x'] < right_bound) &
                            (pos_mpis['rect_y'] > upper_demarcation_line) &
                            (pos_mpis['rect_y'] < demarcation_line)]

        if pos_mpis.empty:
            return relevant_pos

        # remove the placeholder item
        relevant_pos = relevant_pos[~(relevant_pos['product_name'] ==
                                      'Menu POS (Scene Recognized Item)')]

        location_type = 'Header'
        width = 1

        for row in pos_mpis.itertuples():
            relevant_pos.loc[len(relevant_pos), ['product_fk', 'product_name', 'center_x',
                                                 'center_y', 'type', 'width', 'position']] = \
                [row.product_fk, row.product_name, row.rect_x, row.rect_y, location_type, width, position]

        return relevant_pos

    @staticmethod
    def remove_duplicate_pos_tags(relevant_pos_df):
        duplicate_results = \
            relevant_pos_df[relevant_pos_df.duplicated(subset=['left_bound', 'right_bound'], keep=False)]

        duplicate_results_without_other = duplicate_results[
            ~duplicate_results['product_name'].str.contains('Other')]

        results_without_duplicates = \
            relevant_pos_df[~relevant_pos_df.duplicated(subset=['left_bound', 'right_bound'], keep=False)]

        if duplicate_results_without_other.empty:
            return relevant_pos_df[~relevant_pos_df.duplicated(
                subset=['left_bound', 'right_bound'], keep='first')]
        else:
            results = pd.concat(
                [duplicate_results_without_other, results_without_duplicates])
            # we need to sort_index to fix the sort order to reflect center_x values
            return results.drop_duplicates(
                subset=['left_bound', 'right_bound']).sort_index()

    def get_custom_entity_pk(self, name):
        return self.custom_entity_data[self.custom_entity_data['name'] ==
                                       name]['pk'].iloc[0]

    def get_length_of_pos(self, row, longest_shelf, category):
        width_in_facings = len(
            longest_shelf[(longest_shelf['rect_x'] > row['left_bound']) &
                          (longest_shelf['rect_x'] < row['right_bound'])]) + 2
        category_facings = category + ' Facings'
        return self.facings_to_feet_template.loc[(
            self.facings_to_feet_template[category_facings] -
            width_in_facings).abs().argsort()[:1]]['POS Width (ft)'].iloc[0]

    @staticmethod
    def get_longest_shelf_number(relevant_mpis, max_shelves_from_top=3):
        # returns the shelf_number of the longest shelf
        try:
            longest_shelf = \
                relevant_mpis[relevant_mpis['shelf_number'] <= max_shelves_from_top].groupby('shelf_number').agg(
                    {'scene_match_fk': 'count'})['scene_match_fk'].idxmax()
        except ValueError:
            longest_shelf = pd.DataFrame()

        return longest_shelf

    @staticmethod
    def get_most_frequent_scene(relevant_scif):
        try:
            relevant_scene_id = relevant_scif['scene_id'].fillna(
                0).mode().iloc[0]
        except IndexError:
            relevant_scene_id = 0
        return relevant_scene_id

    def get_external_target_data_by_kpi_fk(self, kpi_fk):
        return self.external_targets[self.external_targets['kpi_fk'] ==
                                     kpi_fk].iloc[0]

    def commit(self):
        self.common_v2.commit_results_data()
class LIONJP_SANDToolBox:
    LEVEL1 = 1
    LEVEL2 = 2
    LEVEL3 = 3

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = Common(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.kpi_results_queries = []
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.setup_file = "setup.xlsx"
        self.kpi_sheet = self.get_setup(Consts.KPI_SHEET_NAME)
        self.kpi_template_file = "kpi_template.xlsx"
        self.kpi_template = self.get_kpi_template(Consts.KPI_CONFIG_SHEET)

    def get_kpi_template(self, sheet_name):
        kpi_template_path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), '..', 'Data')
        kpi_template_path = os.path.join(kpi_template_path,
                                         self.kpi_template_file)
        kpi_template = pd.read_excel(kpi_template_path,
                                     sheet_name=sheet_name,
                                     skiprows=1)
        return kpi_template

    def get_kpi_level_2_fk(self, kpi_level_2_type):
        query = \
            """
            SELECT pk FROM static.kpi_level_2
            WHERE type = '{}';
            """.format(kpi_level_2_type)
        data = pd.read_sql_query(query, self.rds_conn.db)
        return None if data.empty else data.values[0][0]

    def get_kpi_name(self, kpi_level_2_fk):
        query = \
            """
            SELECT type kpi_name FROM static.kpi_level_2
            WHERE pk = {};
            """.format(kpi_level_2_fk)
        data = pd.read_sql_query(query, self.rds_conn.db)
        return None if data.empty else data.values[0][0]

    def get_setup(self, sheet_name):
        setup_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                  '..', 'Data')
        setup_path = os.path.join(setup_path, self.setup_file)
        setup = pd.read_excel(setup_path, sheet_name=sheet_name)
        return setup

    def main_calculation(self, *args, **kwargs):
        """
        This function calculates the KPI results.
        """
        try:
            if self.kpi_sheet.empty:
                Log.error("'kpi_list' sheet in setup file is empty.")
                return
            kpi_types = [
                x.strip() for x in self.kpi_sheet[Consts.KPI_TYPE].unique()
            ]

            for kpi_type in kpi_types:
                kpis = self.kpi_sheet[self.kpi_sheet[Consts.KPI_TYPE] ==
                                      kpi_type]
                if kpi_type == Consts.FSOS:
                    self.main_sos_calculations(kpis)
                elif kpi_type == Consts.ADJACENCY:
                    self.main_adjacency_calculations(kpis)
                else:
                    Log.warning(
                        "KPI_TYPE:{kt} not found in setup=>kpi_list sheet.".
                        format(kt=kpi_type))
                    continue
            self.common.commit_results_data()
            return
        except Exception as err:
            Log.error(
                "LionJP KPI calculation failed due to the following error: {}".
                format(err))

    def main_sos_calculations(self, kpis):
        for row_number, row_data in kpis.iterrows():
            if row_data[Consts.KPI_NAME] == Consts.FACINGS_IN_CELL_PER_PRODUCT:
                self.calculate_facings_in_cell_per_product()

    def calculate_facings_in_cell_per_product(self):
        kpi_db = self.kpi_static_data[
            (self.kpi_static_data[Consts.KPI_FAMILY] == Consts.PS_KPI_FAMILY)
            & (self.kpi_static_data[Consts.KPI_NAME_DB] ==
               Consts.FACINGS_IN_CELL_PER_PRODUCT)
            & (self.kpi_static_data['delete_time'].isnull())]

        if kpi_db.empty:
            print("KPI Name:{} not found in DB".format(
                Consts.FACINGS_IN_CELL_PER_PRODUCT))
        else:
            print("KPI Name:{} found in DB".format(
                Consts.FACINGS_IN_CELL_PER_PRODUCT))
            kpi_fk = kpi_db.pk.values[0]
            match_prod_scene_data = self.match_product_in_scene.merge(
                self.products,
                how='left',
                on='product_fk',
                suffixes=('', '_prod'))
            grouped_data = match_prod_scene_data.query(
                '(stacking_layer==1) or (product_type=="POS")').groupby(
                    ['scene_fk', 'bay_number', 'shelf_number', 'product_fk'])
            for data_tup, scene_data_df in grouped_data:
                scene_fk, bay_number, shelf_number, product_fk = data_tup
                facings_count_in_cell = len(scene_data_df)
                cur_template_fk = int(
                    self.scene_info[self.scene_info['scene_fk'] ==
                                    scene_fk].get('template_fk'))
                self.common.write_to_db_result(fk=kpi_fk,
                                               numerator_id=product_fk,
                                               denominator_id=self.store_id,
                                               context_id=cur_template_fk,
                                               numerator_result=bay_number,
                                               denominator_result=shelf_number,
                                               result=facings_count_in_cell,
                                               score=scene_fk)

    def main_adjacency_calculations(self, kpis):
        for row_number, row_data in kpis.iterrows():
            if row_data[
                    Consts.
                    KPI_NAME] == Consts.ADJACENCY_PRODUCT_GROUP_IN_SCENE_TYPE:
                self.calculate_adjacency_per_scene(
                    Consts.ADJACENCY_PRODUCT_GROUP_IN_SCENE_TYPE)
            else:
                Log.warning(
                    "KPI_NAME:{kn} not found in setup=>kpi_list sheet.".format(
                        kn=row_data[Consts.KPI_NAME]))

    @staticmethod
    def exclude_and_include_filter(adj_config):
        allowed_filters = []
        exclude_filters = []

        include_empty = adj_config['include_empty']
        include_irrelevant = adj_config['include_irrelevant']

        if include_empty == "exclude":
            allowed_filters.append("Empty")
        else:
            exclude_filters.append("Empty")

        if include_irrelevant == "exclude":
            allowed_filters.append("Irrelevant")
        else:
            exclude_filters.append("Irrelevant")

        exclude_filters.append("POS")

        if len(allowed_filters) == 0:
            allowed_products_filters = None
        else:
            allowed_products_filters = {"product_type": allowed_filters}

        if len(exclude_filters) == 0:
            exclude_products_filters = None
        else:
            exclude_products_filters = {"product_type": exclude_filters}

        return exclude_products_filters, allowed_products_filters

    def build_entity_groups(self, adj_config, scene_fk):
        extra = []
        # include_empty
        if adj_config['include_empty'] == "exclude":
            extra.append(Consts.GENERAL_EMPTY)
            extra.append(Consts.EMPTY)

        # include_irrelevant
        if adj_config['include_irrelevant'] == "exclude":
            extra.append(Consts.IRRELEVANT)

        # include_others
        if not pd.isnull(adj_config['include_other_ean_codes']):
            include_others = tuple([
                other.strip()
                for other in adj_config['include_other_ean_codes'].split(",")
            ])
            if len(include_others) != 0:
                product_fks = self.get_product_fks(include_others)
                extra.extend(product_fks)

        entity_1_type = adj_config['entity_1_type']
        entity_2_type = adj_config['entity_2_type']
        entity_1_values = [
            item.strip()
            for item in adj_config['entity_1_values'].strip().split(",")
        ]
        entity_2_values = [
            item.strip()
            for item in adj_config['entity_2_values'].strip().split(",")
        ]

        entities = [{
            "entity_name": "entity_1",
            "entity_type": entity_1_type,
            "entity_values": entity_1_values
        }, {
            "entity_name": "entity_2",
            "entity_type": entity_2_type,
            "entity_values": entity_2_values
        }]

        df_entities = pd.DataFrame()
        for entity in entities:
            entity_type = entity['entity_type']
            entity_values = entity['entity_values']
            if entity_type == "product":
                df_entity = self.data_provider.all_products[[
                    'product_fk', 'product_ean_code'
                ]].copy()
                df_entity = df_entity[[
                    'product_fk'
                ]][(df_entity['product_ean_code'].isin(entity_values))]
                df_entity['entity'] = entity['entity_name']
            elif entity_type == "brand":
                df_entity = self.data_provider.all_products[[
                    'product_fk', 'brand_name'
                ]].copy()
                df_entity = df_entity[[
                    'product_fk'
                ]][(df_entity['brand_name'].isin(entity_values))]
                df_entity['entity'] = entity['entity_name']
            elif entity_type == "category":
                df_entity = self.data_provider.all_products[[
                    'product_fk', 'category_name'
                ]].copy()
                df_entity = df_entity[[
                    'product_fk'
                ]][(df_entity['category_name'].isin(entity_values))]
                df_entity['entity'] = entity['entity_name']
            elif entity_type == "sub_category":
                df_entity = self.data_provider.all_products[[
                    'product_fk', 'sub_category_name'
                ]].copy()
                df_entity = df_entity[[
                    'product_fk'
                ]][(df_entity['sub_category_name'].isin(entity_values))]
                df_entity['entity'] = entity['entity_name']
            else:
                Log.error("{} invalid entity_type".format(entity_type))
                return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
            df_entities = df_entities.append(df_entity)

        df_entities = df_entities.reset_index(drop=True)

        product_pks = list(df_entities['product_fk'].unique())

        df_product_pks_in_scene = self.scif[
            (self.scif['scene_id'] == scene_fk)
            & (self.scif['item_id'].isin(product_pks)) &
            (self.scif['facings_ign_stack'] > 0)]

        if df_product_pks_in_scene.empty:
            if is_debug:
                Log.warning("Products:{} not in scene{}:scene_fk".format(
                    product_pks, scene_fk))
            return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
        else:
            df_custom_matches = self.data_provider.matches.copy()
            df_custom_matches = df_custom_matches.merge(df_entities,
                                                        on="product_fk")
            df_custom_matches = df_custom_matches[
                (df_custom_matches['scene_fk'] == scene_fk) &
                # (df_custom_matches['face_count'] > 0) &
                (df_custom_matches['stacking_layer'] == 1)]

            blocking_percentage = adj_config['min_block_percentage']

            if int(adj_config['min_overlap_percentage']) != 0:
                overlap_percentage = int(
                    adj_config['min_overlap_percentage']) / 100.00
            else:
                overlap_percentage = 0.1

            df = df_custom_matches[
                (~df_custom_matches['product_fk'].isin(extra))
                & (df_custom_matches['scene_fk'] == scene_fk)]
            minimum_tags_per_entity = self.get_minimum_facings(
                df, blocking_percentage)
            population = {'entity': list(df_entities['entity'].unique())}
            exclude_filter, allowed_products_filter = self.exclude_and_include_filter(
                adj_config)

        if adj_config['orientation'].lower() == "vertical":
            direction = "DOWN"
        elif adj_config['orientation'].lower() == "horizontal":
            direction = "RIGHT"
        else:
            Log.warning(
                "Invalid direction:{}. Resetting to default orientation RIGHT".
                format(adj_config['orientation']))
            direction = "RIGHT"

        sequence_params = {
            AdditionalAttr.DIRECTION: direction,
            AdditionalAttr.EXCLUDE_FILTER: exclude_filter,
            AdditionalAttr.CHECK_ALL_SEQUENCES: True,
            AdditionalAttr.STRICT_MODE: False,
            AdditionalAttr.REPEATING_OCCURRENCES: True,
            AdditionalAttr.INCLUDE_STACKING: False,
            AdditionalAttr.ALLOWED_PRODUCTS_FILTERS: allowed_products_filter,
            AdditionalAttr.MIN_TAGS_OF_ENTITY: minimum_tags_per_entity,
            AdditionalAttr.ADJACENCY_OVERLAP_RATIO: overlap_percentage
        }

        return population, sequence_params, df_custom_matches

    @staticmethod
    def get_minimum_facings(df, block_percentage):
        facings = 0
        if df.empty:
            return facings

        entities = df.groupby('entity')['product_fk'].count()
        percentage = entities.apply(lambda entity_facings: entity_facings *
                                    (block_percentage / 100.00))
        if len(percentage) > 1:
            facings = percentage.min()
        else:
            # when one of the entity is missing in the scene
            facings = 0

        if (facings > 0) and (facings < 1):
            facings = 1
        else:
            facings = int(facings)
        return facings

    def get_custom_entity_fk(self, name):
        query = """
            SELECT pk FROM static.custom_entity
            WHERE name='{}'
            """.format(name)
        data = pd.read_sql_query(query, self.rds_conn.db)
        return None if data.empty else data.values[0][0]

    def calculate_adjacency_per_scene(self, kpi_name):
        allowed_entities = ["product", "brand", "category", "sub_category"]
        kpi_config = self.kpi_template[self.kpi_template["kpi_name"] ==
                                       kpi_name].copy()
        kpi_config['visit_date'] = pd.Timestamp(self.visit_date)
        kpi_config = kpi_config[kpi_config['visit_date'].between(
            kpi_config['start_date'], kpi_config['end_date'], inclusive=True)]

        kpi_level_2_fk = self.get_kpi_level_2_fk(kpi_name)

        if kpi_config.empty:
            message = "kpi_name:{} ".format(kpi_name)
            message += " not found in static.kpi_level_2 table"
            Log.warning(message)
            return

        for idx, adj_config in kpi_config.iterrows():
            custom_entity_pk = self.get_custom_entity_fk(
                adj_config['report_label'])
            scene_types = [
                x.strip() for x in adj_config['scene_type'].split(",")
            ]
            df_scene_template = self.scif[[
                'scene_fk', 'template_fk'
            ]][self.scif['template_name'].isin(scene_types)]
            df_scene_template = df_scene_template.drop_duplicates()
            for row_num, row_data in df_scene_template.iterrows():
                scene_fk = row_data['scene_fk']
                template_fk = row_data['template_fk']

                if adj_config[
                        'entity_1_type'] in allowed_entities and adj_config[
                            'entity_2_type'] in allowed_entities:
                    location = {"scene_fk": scene_fk}
                    population, sequence_params, custom_matches = self.build_entity_groups(
                        adj_config, scene_fk)

                    if custom_matches.empty:
                        if is_debug:
                            Log.warning(
                                "scene_fk:{}, Custom Entities are not found in the scene"
                                .format(scene_fk))
                        continue

                    if sequence_params[AdditionalAttr.MIN_TAGS_OF_ENTITY] == 0:
                        continue

                    seq = Sequence(self.data_provider, custom_matches)
                    sequence_res = seq.calculate_sequence(
                        population, location, sequence_params)
                    result_count = len(sequence_res)
                    result = 1 if result_count > 0 else 0
                    score = result
                    if result > 0:
                        if is_debug:
                            Log.warning(
                                "scene_fk:{}, report_label:{}, result={}".
                                format(scene_fk, adj_config['report_label'],
                                       result))

                    self.common.write_to_db_result(
                        fk=kpi_level_2_fk,
                        numerator_id=custom_entity_pk,
                        denominator_id=template_fk,
                        context_id=self.store_id,
                        numerator_result=result_count,
                        denominator_result=scene_fk,
                        result=result,
                        score=score,
                        target=sequence_params[
                            AdditionalAttr.MIN_TAGS_OF_ENTITY])
                else:
                    Log.warning("Invalid entity:{}".format(
                        adj_config['entity']))

    def get_product_fks(self, ean_codes):
        product_pks = []
        query = """
            SELECT pk FROM static_new.product
            WHERE ean_code in '{}'
            """.format(ean_codes)
        data = pd.read_sql_query(query, self.rds_conn.db)
        if data.empty:
            return product_pks
        else:
            product_pks = list(data['pk'].unique())
        return product_pks
class ALTRIAUSToolBox:
    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = Common(self.data_provider)
        self.common_v2 = CommonV2(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.template_info = self.data_provider.all_templates
        self.rds_conn = ProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.ps_data_provider = PsDataProvider(self.data_provider)
        self.match_product_in_probe_state_reporting = self.ps_data_provider.get_match_product_in_probe_state_reporting()
        self.kpi_results_queries = []
        self.fixture_width_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE, "Fixture Width", dtype=pd.Int64Dtype())
        self.facings_to_feet_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE, "Conversion Table", dtype=pd.Int64Dtype())
        self.header_positions_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE, "Header Positions")
        self.flip_sign_positions_template = pd.read_excel(FIXTURE_WIDTH_TEMPLATE, "Flip Sign Positions")
        self.custom_entity_data = self.ps_data_provider.get_custom_entities(1005)
        self.ignore_stacking = False
        self.facings_field = 'facings' if not self.ignore_stacking else 'facings_ign_stack'
        self.kpi_new_static_data = self.common.get_new_kpi_static_data()
        try:
            self.mpis = self.match_product_in_scene.merge(self.products, on='product_fk', suffixes=['', '_p']) \
                        .merge(self.scene_info, on='scene_fk', suffixes=['', '_s']) \
                          .merge(self.template_info, on='template_fk', suffixes=['', '_t'])
        except KeyError:
            Log.warning('MPIS cannot be generated!')
            return
        self.adp = AltriaDataProvider(self.data_provider)
        self.active_kpis = self._get_active_kpis()
        self.external_targets = self.ps_data_provider.get_kpi_external_targets()
        self.survey_dot_com_collected_this_session = self._get_survey_dot_com_collected_value()
        self._add_smart_attributes_to_mpis()
        self.scene_graphs = {}
        self.excessive_flipsigns = False
        self.incorrect_tags_in_pos_areas = []

    def _add_smart_attributes_to_mpis(self):
        smart_attribute_data = \
            self.adp.get_match_product_in_probe_state_values(self.mpis['probe_match_fk'].unique().tolist())

        self.mpis = pd.merge(self.mpis, smart_attribute_data, on='probe_match_fk', how='left')
        self.mpis['match_product_in_probe_state_fk'].fillna(0, inplace=True)

    def main_calculation(self, *args, **kwargs):
        """
               This function calculates the KPI results.
               """
        self.generate_graph_per_scene()
        for graph in self.scene_graphs.values():
            self.calculate_fixture_and_block_level_kpis(graph)

        self.calculate_register_type()
        self.calculate_age_verification()
        self.calculate_facings_by_scene_type()

        self.calculate_session_flags()

        return

    def calculate_fixture_and_block_level_kpis(self, graph):
        for node, node_data in graph.nodes(data=True):
            if node_data['category'].value != 'POS':
                self.calculate_fixture_width(node_data)
                self.calculate_flip_sign(node, node_data, graph)
                self.calculate_flip_sign_empty_space(node, node_data, graph)
                self.calculate_flip_sign_locations(node_data)
                self.calculate_total_shelves(node_data)
                self.calculate_tags_by_fixture_block(node_data)
                if node_data['block_number'].value == 1:
                    self.calculate_header(node, node_data, graph)
                    self.calculate_product_above_headers(node_data)
                    self.calculate_no_headers(node_data)
        return

    def generate_graph_per_scene(self):
        relevant_scif = self.scif[self.scif['template_name'] == 'Tobacco Merchandising Space']
        for scene in relevant_scif['scene_id'].unique().tolist():
            agb = AltriaGraphBuilder(self.data_provider, scene)
            self.scene_graphs[scene] = agb.get_graph()
            if agb.incorrect_tags_in_pos_area:
                self.incorrect_tags_in_pos_areas.extend(agb.incorrect_tags_in_pos_area)
        if len(self.scene_graphs.keys()) > 1:
            Log.warning("More than one Tobacco Merchandising Space scene detected. Results could be mixed!")
        return

    def calculate_fixture_width(self, node_data):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Fixture Width')

        width = node_data['calculated_width_ft'].value
        width = round(width)
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value
        category_fk = self.get_category_fk_by_name(node_data['category'].value)

        self.common_v2.write_to_db_result(kpi_fk, numerator_id=category_fk, denominator_id=self.store_id,
                                          numerator_result=block_number, denominator_result=fixture_number,
                                          result=width)

    def calculate_flip_sign(self, node, node_data, graph):
        if node_data['category'].value == 'Cigarettes':
            self.calculate_flip_signs_cigarettes(node, node_data, graph)
        else:
            self.calculate_flip_signs_non_cigarettes(node, node_data, graph)
        return

    def calculate_flip_signs_non_cigarettes(self, node, node_data, graph):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Flip Sign')
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        flip_signs_by_x_coord = {}

        for neighbor in graph.neighbors(node):
            neighbor_data = graph.nodes[neighbor]
            if neighbor_data['category'].value != 'POS':
                continue
            if neighbor_data['pos_type'].value != 'Flip-Sign':
                continue

            center_x = neighbor_data['polygon'].centroid.coords[0][0]

            flip_signs_by_x_coord[center_x] = neighbor_data

        for i, pair in enumerate(sorted(flip_signs_by_x_coord.items())):
            position = i+1
            position_fk = self.get_custom_entity_pk(str(position))
            if position_fk == 0 or position > 8:
                self.excessive_flipsigns = True
                Log.warning('More than 8 flip-sign positions found for a non-cigarettes block')
            product_fk = pair[1]['product_fk'].value
            width = pair[1]['calculated_width_ft'].value
            implied_facings = pair[1]['width_of_signage_in_facings'].value
            width = self.round_threshold(width)

            self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=position_fk,
                                              numerator_result=block_number, denominator_result=fixture_number,
                                              result=width, score=implied_facings)

    def calculate_flip_signs_cigarettes(self, node, node_data, graph):
        width = node_data['calculated_width_ft'].value
        fixture_bounds = node_data['polygon'].bounds
        fixture_width_coord_units = abs(fixture_bounds[0] - fixture_bounds[2])
        proportions_dict = self.get_flip_sign_position_proportions(round(width))
        if not proportions_dict:
            return

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Flip Sign')
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        f_slot_boxes = {}
        left_bound = fixture_bounds[0]  # start proportional divisions on left side of fixture
        for position, proportion in proportions_dict.items():
            right_bound = left_bound + (fixture_width_coord_units * proportion)
            # TODO update this to use min and max height of graph
            f_slot_boxes[position] = box(left_bound, -9999, right_bound, 9999)
            left_bound = right_bound

        for position, slot_box in f_slot_boxes.items():
            for neighbor in graph.neighbors(node):
                neighbor_data = graph.nodes[neighbor]
                try:
                    if neighbor_data['pos_type'].value != 'Flip-Sign':
                        continue
                except KeyError:
                    continue
                flip_sign_bounds = neighbor_data['polygon'].bounds
                flip_sign_box = box(flip_sign_bounds[0], flip_sign_bounds[1], flip_sign_bounds[2], flip_sign_bounds[3])

                overlap_ratio = flip_sign_box.intersection(slot_box).area / flip_sign_box.area
                if overlap_ratio >= REQUIRED_FLIP_SIGN_FSLOT_OVERLAP_RATIO:
                    product_fk = neighbor_data['product_fk'].value
                    position_fk = self.get_custom_entity_pk(position)
                    flip_sign_width = neighbor_data['calculated_width_ft'].value
                    flip_sign_width = self.round_threshold(flip_sign_width)
                    implied_facings = neighbor_data['width_of_signage_in_facings'].value
                    self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=position_fk,
                                                      numerator_result=block_number, denominator_result=fixture_number,
                                                      result=flip_sign_width, score=implied_facings)
        return

    def calculate_flip_sign_empty_space(self, node, node_data, graph):
        if node_data['category'].value != 'Cigarettes':
            return

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Empty Flip Sign Space')
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        fixture_width = node_data['calculated_width_ft'].value
        fixture_width = self.round_threshold(fixture_width)

        flip_sign_widths = []

        for neighbor in graph.neighbors(node):
            neighbor_data = graph.nodes[neighbor]
            if neighbor_data['category'].value != 'POS':
                continue
            if neighbor_data['pos_type'].value != 'Flip-Sign':
                continue
            # exclude 'General POS Other'
            if neighbor_data['product_fk'].value == 9304:
                continue

            neighbor_width = neighbor_data['calculated_width_ft'].value
            neighbor_width = self.round_threshold(neighbor_width)
            flip_sign_widths.append(neighbor_width)

        empty_space = abs(fixture_width - sum(flip_sign_widths))

        self.common_v2.write_to_db_result(kpi_fk, numerator_id=49, denominator_id=self.store_id,
                                          numerator_result=block_number, denominator_result=fixture_number,
                                          result=empty_space)

    def calculate_flip_sign_locations(self, node_data):
        if node_data['category'].value != 'Cigarettes':
            return

        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Flip Sign Locations')

        width = node_data['calculated_width_ft'].value
        width = round(width)

        proportions_dict = self.get_flip_sign_position_proportions(width)
        if not proportions_dict:
            return

        for position, proportion in proportions_dict.items():
            position_fk = self.get_custom_entity_pk(position)
            self.common_v2.write_to_db_result(kpi_fk, numerator_id=49, denominator_id=position_fk,
                                              numerator_result=block_number, denominator_result=fixture_number,
                                              result=round(proportion * width))

        return

    def get_flip_sign_position_proportions(self, width):
        relevant_template = self.fixture_width_template[self.fixture_width_template['Fixture Width (ft)'] == width]
        if relevant_template.empty:
            Log.error("Unable to save flip sign location data. {}ft does not exist as a defined width".format(width))
            return None

        # this could definitely be simpler, but I'm tired and don't want to use my brain
        relevant_template[['F1', 'F2', 'F3', 'F4']] = relevant_template[['F1', 'F2', 'F3', 'F4']].fillna(0)

        f_slots = [relevant_template['F1'].iloc[0], relevant_template['F2'].iloc[0],
                   relevant_template['F3'].iloc[0], relevant_template['F4'].iloc[0]]

        f_slots = [i for i in f_slots if i != 0]

        proportions_dict = {}

        for i, slot in enumerate(f_slots):
            proportions_dict["F{}".format(i+1)] = slot / float(sum(f_slots))

        return proportions_dict

    def calculate_total_shelves(self, node_data):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Total Shelves')

        shelves = len(node_data['shelf_number'].values)
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value
        category_fk = self.get_category_fk_by_name(node_data['category'].value)

        self.common_v2.write_to_db_result(kpi_fk, numerator_id=category_fk, denominator_id=self.store_id,
                                          numerator_result=block_number, denominator_result=fixture_number,
                                          result=shelves)

    def calculate_tags_by_fixture_block(self, node_data):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('FACINGS_BY_FIXTURE_BLOCK')

        scene_match_fks = node_data['match_fk'].values
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        relevant_matches = self.mpis[self.mpis['scene_match_fk'].isin(scene_match_fks)]

        relevant_matches = relevant_matches.groupby(['product_fk', 'shelf_number', 'match_product_in_probe_state_fk'],
                                                    as_index=False).agg({'scene_match_fk': 'min',
                                                                         'probe_match_fk': 'count'})
        relevant_matches.rename(columns={'probe_match_fk': 'facings'}, inplace=True)

        for row in relevant_matches.itertuples():
            self.common_v2.write_to_db_result(kpi_fk, numerator_id=row.product_fk,
                                              denominator_id=row.match_product_in_probe_state_fk,
                                              numerator_result=block_number, denominator_result=fixture_number,
                                              result=row.facings, score=row.shelf_number,
                                              context_id=row.scene_match_fk)

    def calculate_header(self, node, node_data, graph):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Header')
        parent_category = node_data['category'].value
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        if parent_category not in self.header_positions_template.columns.tolist():
            return
        headers_by_x_coord = {}
        menu_board_items_by_x_coord = {}

        for neighbor in graph.neighbors(node):
            neighbor_data = graph.nodes[neighbor]
            if neighbor_data['category'].value != 'POS':
                continue
            if neighbor_data['pos_type'].value != 'Header':
                continue

            center_x = neighbor_data['polygon'].centroid.coords[0][0]

            if neighbor_data['in_menu_board_area'].value is True:
                menu_board_items_by_x_coord[center_x] = neighbor_data
            else:
                headers_by_x_coord[center_x] = neighbor_data

        number_of_headers = len(headers_by_x_coord.keys())
        if menu_board_items_by_x_coord:
            number_of_headers += 1

        relevant_positions = \
            self.header_positions_template[(self.header_positions_template['Number of Headers'] == number_of_headers)]

        if relevant_positions.empty:
            if number_of_headers == 0:
                return
            else:
                Log.error("Too many headers ({}) found for one block ({}). Unable to calculate positions!".format(
                    number_of_headers, parent_category))
                return

        positions = relevant_positions[parent_category].iloc[0]
        positions = [position.strip() for position in positions.split(',')]

        for i, pair in enumerate(sorted(headers_by_x_coord.items())):
            position_fk = self.get_custom_entity_pk(positions[i])
            product_fk = pair[1]['product_fk'].value
            width = pair[1]['calculated_width_ft'].value
            width = self.round_threshold(width)

            self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=position_fk,
                                              numerator_result=block_number, denominator_result=fixture_number,
                                              result=width, score=width)

        if menu_board_items_by_x_coord:
            for pair in menu_board_items_by_x_coord.items():
                position_fk = self.get_custom_entity_pk(positions[-1])
                product_fk = pair[1]['product_fk'].value
                width = pair[1]['calculated_width_ft'].value
                width = self.round_threshold(width)
                # width = 1  # this is because there is no real masking for menu board items

                self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=position_fk,
                                                  numerator_result=block_number, denominator_result=fixture_number,
                                                  result=width, score=width)
        return

    def calculate_product_above_headers(self, node_data):
        try:
            product_above_header = node_data['product_above_header'].value
        except KeyError:
            return

        if not product_above_header:
            return

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Product Above Header')
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        self.common_v2.write_to_db_result(kpi_fk, numerator_id=49, denominator_id=self.store_id,
                                          numerator_result=block_number, denominator_result=fixture_number,
                                          result=1, score=1, context_id=fixture_number)
        return

    def calculate_no_headers(self, node_data):
        try:
            no_header = node_data['no_header'].value
        except KeyError:
            return

        if not no_header:
            return

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('No Header')
        fixture_number = node_data['fixture_number'].value
        block_number = node_data['block_number'].value

        self.common_v2.write_to_db_result(kpi_fk, numerator_id=49, denominator_id=self.store_id,
                                          numerator_result=block_number, denominator_result=fixture_number,
                                          result=1, score=1, context_id=fixture_number)
        return

    def _get_active_kpis(self):
        active_kpis = self.kpi_new_static_data[(self.kpi_new_static_data['kpi_calculation_stage_fk'] == 3) &
                                           (self.kpi_new_static_data['valid_from'] <= self.visit_date) &
                                           ((self.kpi_new_static_data['valid_until']).isnull() |
                                            (self.kpi_new_static_data['valid_until'] >= self.visit_date))]
        return active_kpis

    def calculate_facings_by_scene_type(self):
        kpi_name = 'FACINGS_BY_SCENE_TYPE'
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(kpi_name)
        if kpi_name not in self.active_kpis['type'].unique().tolist():
            return

        config = self.get_external_target_data_by_kpi_fk(kpi_fk)

        product_types = config.product_type
        template_names = config.template_name

        relevant_mpis = self.mpis[(self.mpis['product_type'].isin(product_types)) &
                                  (self.mpis['template_name'].isin(template_names))]

        relevant_mpis = relevant_mpis.groupby(['product_fk',
                                               'template_fk',
                                               'match_product_in_probe_state_fk'],
                                              as_index=False)['scene_match_fk'].count()
        relevant_mpis.rename(columns={'scene_match_fk': 'facings'}, inplace=True)

        for row in relevant_mpis.itertuples():
            self.common_v2.write_to_db_result(kpi_fk, numerator_id=row.product_fk, denominator_id=row.template_fk,
                                              context_id=row.match_product_in_probe_state_fk, result=row.facings)

        return

    def calculate_register_type(self):
        if self.survey_dot_com_collected_this_session:
            return
        relevant_scif = self.scif[(self.scif['product_type'].isin(['POS', 'Other'])) &
                                  (self.scif['category'] == 'POS Machinery')]
        if relevant_scif.empty:
            result = 0
            product_fk = 0
        else:
            result = 1
            product_fk = relevant_scif['product_fk'].iloc[0]

        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Register Type')
        self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=self.store_id,
                                          result=result)

    def calculate_age_verification(self):
        if self.survey_dot_com_collected_this_session:
            return
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Age Verification')
        relevant_scif = self.scif[self.scif['brand_name'].isin(['Age Verification'])]

        if relevant_scif.empty:
            result = 0
            product_fk = 0

            self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=self.store_id,
                                              result=result)
        else:
            result = 1
            for product_fk in relevant_scif['product_fk'].unique().tolist():
                self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=self.store_id,
                                                  result=result)
        return

    def calculate_session_flags(self):
        if self.excessive_flipsigns:
            kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Excessive Flip Signs Detected')
            self.common_v2.write_to_db_result(kpi_fk, numerator_id=49, denominator_id=self.store_id, result=1)
        if self.incorrect_tags_in_pos_areas:
            kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Invalid Product in Header Areas')
            relevant_product_fks = self.mpis[self.mpis['scene_match_fk'].isin(self.incorrect_tags_in_pos_areas)][
                'product_fk'].unique().tolist()
            for product_fk in relevant_product_fks:
                self.common_v2.write_to_db_result(kpi_fk, numerator_id=product_fk, denominator_id=self.store_id,
                                                  result=1)

    def mark_tags_in_explorer(self, probe_match_fk_list, mpipsr_name):
        if not probe_match_fk_list:
            return
        try:
            match_type_fk = \
                self.match_product_in_probe_state_reporting[
                    self.match_product_in_probe_state_reporting['name'] == mpipsr_name][
                    'match_product_in_probe_state_reporting_fk'].values[0]
        except IndexError:
            Log.warning('Name not found in match_product_in_probe_state_reporting table: {}'.format(mpipsr_name))
            return

        match_product_in_probe_state_values_old = self.common_v2.match_product_in_probe_state_values
        match_product_in_probe_state_values_new = pd.DataFrame(columns=[MATCH_PRODUCT_IN_PROBE_FK,
                                                                        MATCH_PRODUCT_IN_PROBE_STATE_REPORTING_FK])
        match_product_in_probe_state_values_new[MATCH_PRODUCT_IN_PROBE_FK] = probe_match_fk_list
        match_product_in_probe_state_values_new[MATCH_PRODUCT_IN_PROBE_STATE_REPORTING_FK] = match_type_fk

        self.common_v2.match_product_in_probe_state_values = pd.concat([match_product_in_probe_state_values_old,
                                                                        match_product_in_probe_state_values_new])

        return

    @staticmethod
    def round_threshold(value, threshold=0.2):
        return round(value - threshold + 0.5)

    def _get_survey_dot_com_collected_value(self):
        try:
            sales_rep_fk = self.session_info['s_sales_rep_fk'].iloc[0]
        except IndexError:
            sales_rep_fk = 0

        return int(sales_rep_fk) == 209050

    def get_category_fk_by_name(self, category_name):
        return self.all_products[self.all_products['category'] == category_name]['category_fk'].iloc[0]

    def get_custom_entity_pk(self, name):
        try:
            return self.custom_entity_data[self.custom_entity_data['name'] == name]['pk'].iloc[0]
        except IndexError:
            Log.error('No custom entity found for {}'.format(name))
            return 0

    def get_external_target_data_by_kpi_fk(self, kpi_fk):
        return self.external_targets[self.external_targets['kpi_fk'] == kpi_fk].iloc[0]

    def commit(self):
        self.common_v2.commit_results_data()
示例#7
0
class ColdCutToolBox:
    LEVEL1 = 1
    LEVEL2 = 2
    LEVEL3 = 3

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.common = Common(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.block = Block(data_provider)
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = self.ps_data_provider.rds_conn
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.session_fk = self.session_info['pk'].values[0]
        self.kpi_results_queries = []
        self.kpi_static_queries = []
        self.own_manufacturer_fk = int(
            self.data_provider.own_manufacturer.param_value.values[0])

        self.adjacency = BlockAdjacency(self.data_provider,
                                        ps_data_provider=self.ps_data_provider,
                                        common=self.common,
                                        rds_conn=self.rds_conn)
        self.eyelight = Eyelight(self.data_provider, self.common,
                                 self.ps_data_provider)
        self.merged_scif_mpis = self.match_product_in_scene.merge(
            self.scif,
            how='left',
            left_on=['scene_fk', 'product_fk'],
            right_on=['scene_fk', 'product_fk'])
        self.targets = self.ps_data_provider.get_kpi_external_targets(
            key_fields=[
                "KPI Type", "Location: JSON", "Config Params: JSON",
                "Dataset 1: JSON", "Dataset 2: JSON"
            ])
        self.results_df = pd.DataFrame(columns=[
            'kpi_name', 'kpi_fk', 'numerator_id', 'numerator_result',
            'context_id', 'denominator_id', 'denominator_result', 'result',
            'score'
        ])
        self.custom_entity_table = self.get_kpi_custom_entity_table()

    def main_calculation(self):
        """
        This function calculates the KPI results.
        """
        relevant_kpi_types = [
            Consts.SOS, Consts.HORIZONTAL_SHELF_POSITION,
            Consts.VERTICAL_SHELF_POSITION, Consts.BLOCKING, Consts.BLOCK_ADJ,
            Consts.BLOCKING_ORIENTATION
        ]

        targets = self.targets[self.targets[Consts.ACTUAL_TYPE].isin(
            relevant_kpi_types)]
        self._calculate_kpis_from_template(targets)
        self.save_results_to_db()
        return

    def calculate_blocking(self, row, df):
        if df.empty:
            return None
        additional_data = row['Config Params: JSON']
        location_data = row['Location: JSON']
        kpi_fk = row['kpi_fk']
        population_data = row['Dataset 1: JSON']['include'][0]
        result_dict_list = self._logic_for_blocking(kpi_fk, population_data,
                                                    location_data,
                                                    additional_data)
        return result_dict_list

    def calculate_blocking_adj(self, row, df):
        result_dict_list = []
        additional_data = row['Config Params: JSON']
        location_data = row['Location: JSON']
        kpi_fk = row['kpi_fk']
        anchor_data = row['Dataset 1: JSON']['include'][0]
        target_data = row['Dataset 2: JSON']['include'][0]

        context_type = additional_data.get('context_type')
        if context_type:
            target_df = ParseInputKPI.filter_df(target_data, self.scif)
            target_values = target_df[context_type].unique().tolist()
            context_values = [
                v for v in df[context_type].unique().tolist()
                if v and pd.notna(v) and v in target_values
            ]
            for context_value in context_values:
                anchor_data.update({context_type: [context_value]})
                target_data.update({context_type: [context_value]})
                result_dict = self._logic_for_adj(
                    kpi_fk,
                    anchor_data,
                    target_data,
                    location_data,
                    additional_data,
                    eyelight_prefix='{}-'.format(context_value),
                    custom_entity=context_value)
                result_dict_list.append(result_dict)
        else:
            result_dict = self._logic_for_adj(kpi_fk, anchor_data, target_data,
                                              location_data, additional_data)
            result_dict_list.append(result_dict)
        return result_dict_list

    def _logic_for_adj(self,
                       kpi_fk,
                       anchor_data,
                       target_data,
                       location_data,
                       additional_data,
                       custom_entity=None,
                       eyelight_prefix=None):
        result = self.adjacency.evaluate_block_adjacency(
            anchor_data,
            target_data,
            location=location_data,
            additional=additional_data,
            kpi_fk=kpi_fk,
            eyelight_prefix=eyelight_prefix)
        result_type_fk = Consts.CUSTOM_RESULT['Yes'] if result and pd.notna(
            result) else Consts.CUSTOM_RESULT['No']
        result_dict = {
            'kpi_fk': kpi_fk,
            'numerator_id': self.own_manufacturer_fk,
            'denominator_id': self.store_id,
            'numerator_result': 1 if result else 0,
            'denominator_result': 1,
            'result': result_type_fk
        }
        if custom_entity:
            result_dict.update(
                {'context_id': self.get_custom_entity_value(custom_entity)})

        return result_dict

    def _logic_for_blocking(self, kpi_fk, population_data, location_data,
                            additional_data):
        result_dict_list = []
        additional_data.update({'use_masking_only': True})
        block = self.block.network_x_block_together(population=population_data,
                                                    location=location_data,
                                                    additional=additional_data)

        for row in block.itertuples():
            scene_match_fks = list(row.cluster.nodes[list(
                row.cluster.nodes())[0]]['scene_match_fk'])
            self.eyelight.write_eyelight_result(scene_match_fks, kpi_fk)
        passed_block = block[block['is_block']]

        if passed_block.empty:
            numerator_result = 0
            result_value = "No"
        else:
            numerator_result = 1
            result_value = "Yes"

        result_type_fk = Consts.CUSTOM_RESULT[result_value]
        # numerator_id = df.custom_entity_fk.iloc[0]

        result_dict = {
            'kpi_fk': kpi_fk,
            'numerator_id': self.own_manufacturer_fk,
            'numerator_result': numerator_result,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'result': result_type_fk
        }

        result_dict_list.append(result_dict)
        return result_dict_list

    def calculate_blocking_orientation(self, row, df):
        if df.empty:
            return
        result_dict_list = []
        additional_data = row['Config Params: JSON']
        location_data = row['Location: JSON']
        kpi_fk = row['kpi_fk']
        population_data = row['Dataset 1: JSON']
        if population_data:
            population_data = population_data['include'][0]
        else:
            population_data = {}

        additional_data.update({
            'vertical_horizontal_methodology':
            ['bucketing', 'percentage_of_shelves'],
            'shelves_required_for_vertical':
            .8,
            'check_vertical_horizontal':
            True
        })

        numerator_type = additional_data.get('numerator_type')
        if numerator_type:
            numerator_values = [
                v for v in df[numerator_type].unique().tolist()
                if v and pd.notna(v)
            ]
            for numerator_value in numerator_values:
                population_data.update({numerator_type: [numerator_value]})
                result_dict = self._logic_for_blocking_orientation(
                    kpi_fk, population_data, location_data, additional_data,
                    numerator_value)
                result_dict_list.append(result_dict)
        else:
            result_dict = self._logic_for_blocking_orientation(
                kpi_fk, population_data, location_data, additional_data)
            result_dict_list.append(result_dict)

        return result_dict_list

    def _logic_for_blocking_orientation(self,
                                        kpi_fk,
                                        population_data,
                                        location_data,
                                        additional_data,
                                        custom_entity=None):
        additional_data.update({'use_masking_only': True})
        block = self.block.network_x_block_together(population=population_data,
                                                    location=location_data,
                                                    additional=additional_data)
        if custom_entity:
            prefix = '{}-'.format(custom_entity)
            numerator_id = self.get_custom_entity_value(custom_entity)
        else:
            prefix = None
            numerator_id = self.own_manufacturer_fk
        for row in block.itertuples():

            scene_match_fks = list(row.cluster.nodes[list(
                row.cluster.nodes())[0]]['scene_match_fk'])
            self.eyelight.write_eyelight_result(scene_match_fks,
                                                kpi_fk,
                                                prefix=prefix)
        passed_block = block[block['is_block']]

        if passed_block.empty:
            result_value = "Not Blocked"
        else:
            result_value = passed_block.orientation.iloc[0]

        result = Consts.CUSTOM_RESULT[result_value]
        result_dict = {
            'kpi_fk': kpi_fk,
            'numerator_id': numerator_id,
            'numerator_result': 1 if result_value != 'Not Blocked' else 0,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'result': result
        }

        return result_dict

    def calculate_vertical_position(self, row, df):
        result_dict_list = []
        mpis = df  # get this from the external target filter_df method thingy
        scene_facings_df = mpis.groupby(['scene_fk', 'product_fk'],
                                        as_index=False)['facings'].max()
        scene_facings_df.rename(columns={'facings': 'scene_facings'},
                                inplace=True)
        shelf_df = self.merged_scif_mpis.groupby(
            ['scene_fk', 'bay_number'],
            as_index=False)['shelf_number_from_bottom'].max()
        shelf_df.rename(columns={'shelf_number_from_bottom': 'shelf_count'},
                        inplace=True)

        pre_sort_mpis = pd.merge(mpis,
                                 scene_facings_df,
                                 how='left',
                                 on=['scene_fk', 'product_fk'])
        scene_facings_df_sorted = pre_sort_mpis.sort_values('scene_facings')
        mpis = scene_facings_df_sorted.drop_duplicates(
            ['scene_fk', 'product_fk'], keep="last")

        mpis = pd.merge(mpis,
                        shelf_df,
                        how='left',
                        on=['scene_fk', 'bay_number'])

        mpis['position'] = mpis.apply(self._calculate_vertical_position,
                                      axis=1)
        mpis['result_type_fk'] = mpis['position'].apply(
            lambda x: Consts.CUSTOM_RESULT.get(x, 0))
        mpis = mpis.groupby(['product_fk'],
                            as_index=False)['result_type_fk'].agg(
                                lambda x: pd.Series.mode(x).iat[0])

        for result in mpis.itertuples():
            custom_fk_result = result.result_type_fk

            if type(custom_fk_result) == numpy.ndarray:
                custom_fk_result = result.result_type_fk[0]

            result_item = {
                'kpi_fk': row.kpi_fk,
                'numerator_id': result.product_fk,
                'numerator_result': 1,
                'denominator_id': self.store_id,
                'denominator_result': 1,
                'result': custom_fk_result,
                'score': 0
            }

            result_dict_list.append(result_item)
        return result_dict_list

    def calculate_horizontal_position(self, row, df):
        result_dict_list = []
        mpis = df  # get this from the external target filter_df method thingy

        scene_facings_df = mpis.groupby(['scene_fk', 'product_fk'],
                                        as_index=False)['facings'].max()
        scene_facings_df.rename(columns={'facings': 'scene_facings'},
                                inplace=True)
        pre_sort_mpis = pd.merge(mpis,
                                 scene_facings_df,
                                 how='left',
                                 on=['scene_fk', 'product_fk'])

        bay_df = pre_sort_mpis.groupby('scene_fk',
                                       as_index=False)['bay_number'].max()
        bay_df.rename(columns={'bay_number': 'bay_count'}, inplace=True)
        mpis = pd.merge(pre_sort_mpis, bay_df, how='left', on='scene_fk')
        mpis['position'] = mpis.apply(self._calculate_horizontal_position,
                                      axis=1)
        mpis['result_type_fk'] = mpis['position'].apply(
            lambda x: Consts.CUSTOM_RESULT.get(x, 0))
        mpis = mpis.groupby(['product_fk'],
                            as_index=False)['result_type_fk'].agg(
                                lambda x: pd.Series.mode(x).iat[0])

        for result in mpis.itertuples():
            custom_fk_result = result.result_type_fk

            if type(custom_fk_result) == numpy.ndarray:
                custom_fk_result = result.result_type_fk[0]

            result_item = {
                'kpi_fk': row.kpi_fk,
                'numerator_id': result.product_fk,
                'numerator_result': 1,
                'denominator_id': self.store_id,
                'denominator_result': 1,
                'result': custom_fk_result,
                'score': 0
            }

            result_dict_list.append(result_item)
        return result_dict_list

    @staticmethod
    def _calculate_horizontal_position(row):
        bay_count = row.bay_count
        if bay_count == 1:
            return 'Center'
        factor = round(bay_count / float(3))
        if row.bay_number <= factor:
            return 'Left'
        elif row.bay_number > (bay_count - factor):
            return 'Right'
        return 'Center'

    @staticmethod
    def _calculate_vertical_position(row):
        shelf_number = str(row.shelf_number_from_bottom)
        shelf_count = str(row.shelf_count)

        shelf_count_pos_map = Consts.shelf_map[shelf_count]
        pos_value = shelf_count_pos_map[shelf_number]

        return pos_value

    def calculate_facings_sos(self, row, df):
        data_filter = {'population': row['Dataset 2: JSON']}
        if 'include' not in data_filter['population'].keys():
            data_filter['population'].update(
                {'include': [{
                    'session_id': self.session_fk
                }]})
        data_filter.update({'location': row['Location: JSON']})
        config_json = row['Config Params: JSON']
        numerator_type = config_json['numerator_type']
        df = ParseInputKPI.filter_df(data_filter, self.scif)
        result_dict_list = self._logic_for_sos(row, df, numerator_type)
        return result_dict_list

    def _logic_for_sos(self, row, df, numerator_type):
        result_list = []
        facing_type = 'facings'
        config_json = row['Config Params: JSON']

        if 'include_stacking' in config_json:
            if config_json['include_stacking']:
                facing_type = 'facings_ign_stack'

        for num_item in df[numerator_type].unique().tolist():
            if num_item:
                numerator_scif = df[df[numerator_type] == num_item]
            else:
                numerator_scif = df[df[numerator_type].isnull()]
                num_item = 'None'

            numerator_result = numerator_scif[facing_type].sum()
            denominator_result = df[facing_type].sum()
            custom_entity_fk = self.get_custom_entity_value(num_item)
            sos_value = self.calculate_percentage_from_numerator_denominator(
                numerator_result, denominator_result)

            result_dict = {
                'kpi_fk': row.kpi_fk,
                'numerator_id': custom_entity_fk,
                'numerator_result': numerator_result,
                'denominator_id': self.store_id,
                'denominator_result': denominator_result,
                'result': sos_value
            }

            result_list.append(result_dict)
        return result_list

    def _get_calculation_function_by_kpi_type(self, kpi_type):
        if kpi_type == Consts.SOS:
            return self.calculate_facings_sos
        elif kpi_type == Consts.HORIZONTAL_SHELF_POSITION:
            return self.calculate_horizontal_position
        elif kpi_type == Consts.VERTICAL_SHELF_POSITION:
            return self.calculate_vertical_position
        elif kpi_type == Consts.BLOCKING:
            return self.calculate_blocking
        elif kpi_type == Consts.BLOCK_ADJ:
            return self.calculate_blocking_adj
        elif kpi_type == Consts.BLOCKING_ORIENTATION:
            return self.calculate_blocking_orientation

    def _calculate_kpis_from_template(self, template_df):
        for i, row in template_df.iterrows():
            try:

                calculation_function = self._get_calculation_function_by_kpi_type(
                    row[Consts.ACTUAL_TYPE])
                row = self.apply_json_parser(row)
                merged_scif_mpis = self._parse_json_filters_to_df(row)
                result_data = calculation_function(row, merged_scif_mpis)
                if result_data and isinstance(result_data, list):
                    for result in result_data:
                        self.results_df.loc[len(self.results_df),
                                            result.keys()] = result
                elif result_data and isinstance(result_data, dict):
                    self.results_df.loc[len(self.results_df),
                                        result_data.keys()] = result_data
            except Exception as e:
                Log.error('Unable to calculate {}: {}'.format(
                    row[Consts.KPI_NAME], e))

    def _parse_json_filters_to_df(self, row):
        jsonv = row[(row.index.str.contains('JSON'))
                    & (~row.index.str.contains('Config Params')) &
                    (~row.index.str.contains('Dataset 2'))]
        filter_json = jsonv[~jsonv.isnull()]
        filtered_scif_mpis = self.merged_scif_mpis
        for each_json in filter_json:
            final_json = {
                'population': each_json
            } if ('include' or 'exclude') in each_json else each_json
            filtered_scif_mpis = ParseInputKPI.filter_df(
                final_json, filtered_scif_mpis)
        if 'include_stacking' in row['Config Params: JSON'].keys():
            including_stacking = row['Config Params: JSON'][
                'include_stacking'][0]
            filtered_scif_mpis[Consts.FINAL_FACINGS] = \
                filtered_scif_mpis.facings if including_stacking == 'True' else filtered_scif_mpis.facings_ign_stack
            filtered_scif_mpis = filtered_scif_mpis[
                filtered_scif_mpis.stacking_layer == 1]
        return filtered_scif_mpis

    def apply_json_parser(self, row):
        json_relevent_rows_with_parse_logic = row[
            (row.index.str.contains('JSON')) & (row.notnull())].apply(
                self.parse_json_row)
        row = row[~row.index.isin(json_relevent_rows_with_parse_logic.index
                                  )].append(
                                      json_relevent_rows_with_parse_logic)
        return row

    def parse_json_row(self, item):
        '''
        :param item: improper json value (formatted incorrectly)
        :return: properly formatted json dictionary
        The function will be in conjunction with apply. The function will applied on the row(pandas series). This is
            meant to convert the json comprised of improper format of strings and lists to a proper dictionary value.
        '''

        if item:
            try:
                container = self.prereq_parse_json_row(item)
            except Exception as e:
                container = None
                Log.warning('{}: Unable to parse json for: {}'.format(e, item))
        else:
            container = None

        return container

    def save_results_to_db(self):
        self.results_df.drop(columns=['kpi_name'], inplace=True)
        self.results_df.rename(columns={'kpi_fk': 'fk'}, inplace=True)
        self.results_df['result'].fillna(0, inplace=True)
        self.results_df['score'].fillna(0, inplace=True)
        results = self.results_df.to_dict('records')
        for result in results:
            result = simplejson.loads(simplejson.dumps(result,
                                                       ignore_nan=True))
            self.common.write_to_db_result(**result)

    @staticmethod
    def prereq_parse_json_row(item):
        '''
        primarly logic for formatting the value of the json
        '''

        container = dict()
        try:
            container = ast.literal_eval(item)
        except:
            json_str = ",".join(item)
            json_str_fixed = json_str.replace("'", '"')
            container = json.loads(json_str_fixed)

        return container

    @staticmethod
    def _get_numerator_and_denominator_type(config_param,
                                            context_relevant=False):
        numerator_type = config_param['numerator_type'][0]
        denominator_type = config_param['denominator_type'][0]
        if context_relevant:
            context_type = config_param['context_type'][0]
            return numerator_type, denominator_type, context_type
        return numerator_type, denominator_type

    @staticmethod
    def calculate_percentage_from_numerator_denominator(
            numerator_result, denominator_result):
        try:
            ratio = numerator_result / denominator_result
        except Exception as e:
            Log.error(e.message)
            ratio = 0
        if not isinstance(ratio, (float, int)):
            ratio = 0
        return round(ratio * 100, 2)

    def get_kpi_custom_entity_table(self):
        """
        :param entity_type: pk of entity from static.entity_type
        :return: the DF of the static.custom_entity of this entity_type
        """
        query = "SELECT pk, name, entity_type_fk FROM static.custom_entity;"
        df = pd.read_sql_query(query, self.rds_conn.db)
        return df

    def get_custom_entity_value(self, value):
        try:
            custom_fk = self.custom_entity_table['pk'][
                self.custom_entity_table['name'] == value].iloc[0]
            return custom_fk
        except IndexError:
            Log.error('No custom entity found for: {}'.format(value))
            return None

    def commit_results(self):
        self.common.commit_results_data()
示例#8
0
class PsApacGSKAUSceneToolBox:

    def __init__(self, data_provider, output, common):
        self.output = output
        self.data_provider = data_provider
        self.common = common
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.templates = self.data_provider[Data.TEMPLATES]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.scif = self.data_provider.scene_item_facts
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_id = self.store_info.iloc[0].store_fk
        self.store_type = self.data_provider.store_type
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.match_display_in_scene = self.data_provider.match_display_in_scene

    def calculate_display_compliance(self):
        kpi_display_presence = self.kpi_static_data[
            (self.kpi_static_data[KPI_TYPE_COL] == GSK_DISPLAY_PRESENCE)
            & (self.kpi_static_data['delete_time'].isnull())]
        kpi_display_sku_compliance = self.kpi_static_data[
            (self.kpi_static_data[KPI_TYPE_COL] == GSK_DISPLAY_SKU_COMPLIANCE)
            & (self.kpi_static_data['delete_time'].isnull())]
        kpi_display_price_compliance = self.kpi_static_data[
            (self.kpi_static_data[KPI_TYPE_COL] == GSK_DISPLAY_PRICE_COMPLIANCE)
            & (self.kpi_static_data['delete_time'].isnull())]
        kpi_display_bay_purity = self.kpi_static_data[
            (self.kpi_static_data[KPI_TYPE_COL] == GSK_DISPLAY_BAY_PURITY)
            & (self.kpi_static_data['delete_time'].isnull())]
        kpi_display_presence_sku = self.kpi_static_data[
            (self.kpi_static_data[KPI_TYPE_COL] == GSK_DISPLAY_PRESENCE_SKU)
            & (self.kpi_static_data['delete_time'].isnull())]
        secondary_display_targets = self.targets[
            self.targets['kpi_fk'] == kpi_display_presence['pk'].iloc[0]]
        # if no targets return
        if secondary_display_targets.empty:
            Log.warning('There is no target policy for calculating secondary display compliance.')
            return False
        else:
            current_scene_fk = self.scene_info.iloc[0].scene_fk
            display_per_sku_per_scene_calculated = False
            target_matched = False
            for idx, each_target in secondary_display_targets.iterrows():
                if target_matched:
                    Log.info('The session: {sess} - scene: {scene} has matched one target '
                             'and won\'t run for another.'
                             .format(sess=self.session_uid, scene=current_scene_fk))
                    continue
                # loop through each external target to fit the current store
                has_posm_recognized = False
                multi_posm_or_bay = False
                mandatory_sku_compliance = False
                optional_sku_compliance = False
                price_compliance = False
                is_scene_relevant = False
                scene_relevant_targets = pd.DataFrame()
                # check store relevance
                store_relevant_targets = each_target[STORE_IDENTIFIERS].dropna()
                _bool_store_check_df = self.store_info[list(store_relevant_targets.keys())] \
                                       == store_relevant_targets.values
                is_store_relevant = _bool_store_check_df.all(axis=None)
                if is_store_relevant:
                    # check scene relevance
                    scene_relevant_targets = each_target[SCENE_IDENTIFIERS].dropna()
                    _bool_scene_check_df = self.scene_info[list(scene_relevant_targets.keys())] \
                                           == scene_relevant_targets.values
                    is_scene_relevant = _bool_scene_check_df.all(axis=None)
                if is_store_relevant and is_scene_relevant and not target_matched:
                    # calculate display compliance for the matched external targets
                    target_matched = True
                    Log.info('The session: {sess} - scene: {scene} is relevant for calculating '
                             'secondary display compliance.'
                             .format(sess=self.session_uid, scene=current_scene_fk))
                    posm_relevant_targets = each_target[POSM_IDENTIFIERS].dropna()
                    mandatory_eans = _sanitize_csv(posm_relevant_targets[MANDATORY_EANS_KEY])
                    optional_posm_eans = []
                    if OPTIONAL_EAN_KEY in posm_relevant_targets:
                        optional_posm_eans = _sanitize_csv(posm_relevant_targets[OPTIONAL_EAN_KEY])
                    # save detailed sku presence
                    posm_to_check = each_target[POSM_PK_KEY]
                    # FIND THE SCENES WHICH HAS THE POSM to check for multiposm or multibays
                    is_posm_absent = self.match_display_in_scene[
                        self.match_display_in_scene['display_fk'] == posm_to_check].empty
                    if is_posm_absent:
                        Log.info('The scene: {scene} is relevant but POSM {pos} is not present. '
                                 'Save and start new scene.'
                                 .format(scene=current_scene_fk, pos=posm_to_check))
                        # calculate display per sku -- POSM is absent
                        if not display_per_sku_per_scene_calculated:
                            display_per_sku_per_scene_calculated = self.save_display_presence_per_sku(
                                kpi=kpi_display_presence_sku,
                                numerator_result=0,  # 0 posm not recognized
                            )
                        if len(self.match_product_in_scene['bay_number'].unique()) > 1 or \
                                len(self.match_display_in_scene) > 1:
                            Log.info(
                                'The scene: {scene} is relevant and multi_bay_posm is True. '
                                'Purity per bay is calculated and going to next scene.'
                                    .format(scene=current_scene_fk, pos=posm_to_check))
                            multi_posm_or_bay = True
                            self.save_purity_per_bay(kpi_display_bay_purity)
                        self.save_display_compliance_data(
                            [
                                {'pk': kpi_display_presence.iloc[0].pk, 'result': int(has_posm_recognized),
                                 'score': int(multi_posm_or_bay), 'numerator_id': posm_to_check,
                                 'numerator_result': posm_to_check},
                                {'pk': kpi_display_sku_compliance.iloc[0].pk, 'result': float(mandatory_sku_compliance),
                                 'score': float(optional_sku_compliance), 'denominator_id': posm_to_check,
                                 'denominator_result': posm_to_check},
                                {'pk': kpi_display_price_compliance.iloc[0].pk, 'result': float(price_compliance),
                                 'score': float(price_compliance), 'denominator_id': posm_to_check,
                                 'denominator_result': posm_to_check},
                            ]
                        )
                        continue
                    # this scene has the posm
                    Log.info('The scene: {scene} is relevant and POSM {pos} is present.'
                             .format(scene=current_scene_fk, pos=posm_to_check))
                    has_posm_recognized = True
                    # check if this scene has multi bays or multi posm
                    if len(self.match_product_in_scene['bay_number'].unique()) > 1 or \
                            len(self.match_display_in_scene) > 1:
                        # Its multi posm or bay -- only purity calc per bay is possible
                        Log.info('The scene: {scene} is relevant and POSM {pos} is present but multi_bay_posm is True. '
                                 'Purity per bay is calculated and going to next scene.'
                                 .format(scene=current_scene_fk, pos=posm_to_check))
                        multi_posm_or_bay = True
                        # calculate display per sku for multi posm/multi bay
                        if not display_per_sku_per_scene_calculated:
                            display_per_sku_per_scene_calculated = self.save_display_presence_per_sku(
                                kpi=kpi_display_presence_sku,
                                numerator_result=2,  # 2 multi posm
                            )
                        self.save_display_compliance_data(
                            [
                                {'pk': kpi_display_presence.iloc[0].pk, 'result': int(has_posm_recognized),
                                 'score': int(multi_posm_or_bay), 'numerator_id': posm_to_check,
                                 'numerator_result': posm_to_check},
                                {'pk': kpi_display_sku_compliance.iloc[0].pk, 'result': float(mandatory_sku_compliance),
                                 'score': float(optional_sku_compliance), 'denominator_id': posm_to_check,
                                 'denominator_result': posm_to_check},
                                {'pk': kpi_display_price_compliance.iloc[0].pk, 'result': float(price_compliance),
                                 'score': float(price_compliance), 'denominator_id': posm_to_check,
                                 'denominator_result': posm_to_check},
                            ]
                        )
                        self.save_purity_per_bay(kpi_display_bay_purity)
                        continue

                    Log.info('The scene: {scene} is relevant and POSM {pos} is present with only one bay.'
                             .format(scene=current_scene_fk, pos=posm_to_check))
                    # save purity per bay
                    self.save_purity_per_bay(kpi_display_bay_purity)
                    # calculate display per sku for ALL SUCCESS
                    if not display_per_sku_per_scene_calculated:
                        display_per_sku_per_scene_calculated = self.save_display_presence_per_sku(
                            kpi=kpi_display_presence_sku,
                            posm_to_check=posm_to_check,
                            numerator_result=1,  # 1--one one posm
                            mandatory_eans=mandatory_eans,
                            optional_posm_eans=optional_posm_eans)
                    # calculate compliance
                    mandatory_sku_compliance = self.get_ean_presence_rate(mandatory_eans)
                    optional_sku_compliance = self.get_ean_presence_rate(optional_posm_eans)
                    if mandatory_sku_compliance:
                        price_compliance = self.get_price_presence_rate(mandatory_eans)
                    self.save_display_compliance_data(
                        [
                            {'pk': kpi_display_presence.iloc[0].pk, 'result': int(has_posm_recognized),
                             'score': int(multi_posm_or_bay), 'numerator_id': posm_to_check,
                             'numerator_result': posm_to_check},
                            {'pk': kpi_display_sku_compliance.iloc[0].pk, 'result': float(mandatory_sku_compliance),
                             'score': float(optional_sku_compliance), 'denominator_id': posm_to_check,
                             'denominator_result': posm_to_check},
                            {'pk': kpi_display_price_compliance.iloc[0].pk, 'result': float(price_compliance),
                             'score': float(price_compliance), 'denominator_id': posm_to_check,
                             'denominator_result': posm_to_check},
                        ]
                    )
                    continue
                else:
                    # the session/store is not part of the KPI targets
                    Log.info('The session: {sess} - scene: {scene}, the current kpi target [pk={t_pk}] '
                             'is not valid. Keep Looking...'
                             .format(sess=self.session_uid, scene=current_scene_fk,
                                     t_pk=each_target.external_target_fk))
                    if scene_relevant_targets.empty:
                        # Store failed
                        Log.info("Store info is {curr_data} but target is {store_data}".format(
                            curr_data=self.store_info.iloc[0][list(store_relevant_targets.keys())].to_json(),
                            store_data=store_relevant_targets.to_json()
                        ))
                    else:
                        Log.info("Scene info is {curr_data} but target is {store_data}".format(
                            curr_data=self.scene_info.iloc[0][list(scene_relevant_targets.keys())].to_json(),
                            store_data=scene_relevant_targets.to_json()
                        ))
                    continue
            else:
                if not display_per_sku_per_scene_calculated:
                    # check if its secondary display type
                    if not self.templates.loc[(self.templates['template_group'] == 'Secondary display') &
                                              (~self.templates['template_name'].isin(['Clipstrip', 'Hangsell']))].empty:
                        Log.info("Secondary Display => Session: {sess} - scene {scene} didn't qualify "
                                 "any external targets.".format(sess=self.session_uid,
                                                                scene=self.scene_info.iloc[0].scene_fk,
                                                                ))
                        display_per_sku_per_scene_calculated = self.save_display_presence_per_sku(
                            kpi=kpi_display_presence_sku,
                            numerator_result=0)  # 0--posm not recognized

    def calculate_layout_compliance(self):
        current_scene_fk = self.scene_info.iloc[0].scene_fk
        Log.info('Calculate Layout Compliance for session: {sess} - scene: {scene}'
                 .format(sess=self.session_uid, scene=current_scene_fk))
        scene_layout_calc_obj = SceneLayoutComplianceCalc(scene_toolbox_obj=self)
        scene_layout_calc_obj.calculate_all()

    def get_ean_presence_rate(self, ean_list):
        """
        This method takes the list of eans, checks availability in scif and returns percentage
        of items among the input list; which are present
        """
        Log.info('Calculate ean presence rate for : {scene}.'.format(scene=self.scene_info.iloc[0].scene_fk))
        if not ean_list:
            return 0.0
        present_ean_count = len(self.scif[self.scif['product_ean_code'].isin(ean_list)])
        return present_ean_count / float(len(ean_list)) * 100

    def get_price_presence_rate(self, ean_list):
        """
        This method takes ean list as input and returns percentage of eans which has price.
        """
        Log.info('Calculate price presence rate for : {scene}.'.format(scene=self.scene_info.iloc[0].scene_fk))
        if not ean_list:
            return 0.0
        scif_to_check = self.scif[self.scif['product_ean_code'].isin(ean_list)]
        price_fields = ['median_price', 'median_promo_price']
        present_price_count = 0
        for idx, each_data in scif_to_check.iterrows():
            if each_data[price_fields].apply(pd.notnull).any():
                present_price_count += 1
        return present_price_count / float(len(ean_list)) * 100

    def save_purity_per_bay(self, kpi_bay_purity):
        Log.info('Calculate purity per bay for : {scene}.'.format(scene=self.scene_info.iloc[0].scene_fk))
        mpis_grouped_by_bay = self.match_product_in_scene.groupby(['bay_number'])
        for bay_number, mpis in mpis_grouped_by_bay:
            total_prod_in_bay_count = len(
                mpis[mpis['product_fk'] != 0]
            )
            mpis_with_prod = mpis.merge(self.products, how='left', on=['product_fk'], suffixes=('', '_prod'))
            gsk_prod_count = len(mpis_with_prod[mpis_with_prod['manufacturer_fk'] == 2])
            if total_prod_in_bay_count:
                purity = gsk_prod_count / float(total_prod_in_bay_count) * 100
                Log.info('Save purity per bay for scene: {scene}; bay: {bay} & purity: {purity}.'
                         .format(scene=self.scene_info.iloc[0].scene_fk,
                                 bay=bay_number,
                                 purity=purity
                                 ))
                if not bay_number or np.isnan(bay_number):
                    bay_number = -1
                self.common.write_to_db_result(
                    fk=kpi_bay_purity.iloc[0].pk,
                    context_id=self.store_id,
                    numerator_id=self.store_id,
                    denominator_id=self.store_id,
                    numerator_result=self.store_id,
                    denominator_result=self.store_id,
                    result=purity,
                    score=bay_number,
                    by_scene=True,
                )
        return True

    def save_display_compliance_data(self, data):
        for each_kpi_data in data:
            Log.info("Saving Display Compliance kpi_fk {pk} for session: {sess} - scene {scene}".format(
                pk=each_kpi_data.get('pk'),
                sess=self.session_uid,
                scene=self.scene_info.iloc[0].scene_fk,
            ))
            score = each_kpi_data.get('score')
            result = each_kpi_data.get('result')
            if not score or np.isnan(score):
                score = -1
            if not result or np.isnan(result):
                result = 0
            self.common.write_to_db_result(
                fk=each_kpi_data.get('pk'),
                numerator_id=each_kpi_data.get('numerator_id', self.store_id),
                denominator_id=each_kpi_data.get('denominator_id', self.store_id),
                numerator_result=each_kpi_data.get('numerator_result', self.store_id),
                denominator_result=each_kpi_data.get('denominator_result', self.store_id),
                result=result,
                score=score,
                context_id=self.store_id,
                by_scene=True,
            )
        return True

    def save_display_presence_per_sku(self, kpi, posm_to_check=None, numerator_result=None,
                                      mandatory_eans=None, optional_posm_eans=None):
        # This should be done once only per scene
        current_scene_fk = self.scene_info.iloc[0].scene_fk
        context_fk = self.scene_info.iloc[0].template_fk
        if posm_to_check and (mandatory_eans or optional_posm_eans):
            # the scene is valid as per external targets template
            # PER SCENE which are secondary display
            # save POSM as numerator; each product as denominator and template as context
            Log.info('Calculate display presence per sku. The session: {sess} - scene: {scene} is valid.'
                     .format(sess=self.session_uid, scene=current_scene_fk))
            numerator_id = posm_to_check
        else:
            # the scene doesn't have mandatory or optional eans
            # PER SCENE which are secondary display
            # save numerator as empty; each empty as denominator and template as context
            Log.info('Calculate display presence per sku. The session: {sess} - scene: {scene} is invalid.'
                     .format(sess=self.session_uid, scene=current_scene_fk))
            numerator_id = 0  # General Empty
        # result => [ 0=Optional, 1=mandatory, 2=NA]
        # numerator_result => [0-- posm not recognized; 1--one one posm;  2--multi posm]
        for idx, each_row in self.scif.iterrows():
            result = 2  # NA
            if mandatory_eans and each_row.product_ean_code in mandatory_eans:
                result = 1  # mandatory
            elif optional_posm_eans and each_row.product_ean_code in optional_posm_eans:
                result = 0  # optional
            score = min(each_row.median_price, each_row.median_promo_price)
            if not score or np.isnan(score):
                score = -1
            self.common.write_to_db_result(
                fk=kpi.iloc[0].pk,
                numerator_id=numerator_id,  # its the POSM to check or {General Empty if not recognized or multi}
                numerator_result=numerator_result,  # whether POSM is 0, 1 or 2
                denominator_id=each_row.item_id,  # each product in scene
                score=score,  # -1 means not saved in DB
                result=result,  # 0-optional, 1-mandatory, 2- NA
                context_id=context_fk,  # template of scene
                by_scene=True,
            )
        # Save All Displays in the scene
        Log.info('Save all displays in the scene. The session: {sess} - scene: {scene}.'
                 .format(sess=self.session_uid, scene=current_scene_fk))
        kpi_all_displays_in_scene = self.kpi_static_data[
            (self.kpi_static_data[KPI_TYPE_COL] == GSK_DISPLAYS_ALL_IN_SCENE)
            & (self.kpi_static_data['delete_time'].isnull())]
        for idx, each_row in self.match_display_in_scene.iterrows():
            self.common.write_to_db_result(
                fk=kpi_all_displays_in_scene.iloc[0].pk,
                numerator_id=each_row.display_fk,  # the Display/POSM present in the scene
                numerator_result=1,  # no meaning
                denominator_id=each_row.display_brand_fk,  # display brand
                score=1,  # no meaning
                result=1,  # no meaning
                context_id=self.store_id,  # template of scene
                by_scene=True,
            )

        return True
class SINOTHSceneToolBox:
    def __init__(self, data_provider, output, common):
        self.output = output
        self.data_provider = data_provider
        self.common = common
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.templates = self.data_provider[Data.TEMPLATES]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.scif = self.data_provider.scene_item_facts
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.current_scene_fk = self.scene_info.iloc[0].scene_fk
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_id = self.store_info.iloc[0].store_fk
        self.store_type = self.data_provider.store_type
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.match_display_in_scene = self.data_provider.match_display_in_scene
        self.scene_template_info = self.scif[[
            'scene_fk', 'template_fk', 'template_name'
        ]].drop_duplicates()
        self.kpi_template_path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), '..',
            TEMPLATE_PARENT_FOLDER, TEMPLATE_NAME)
        self.kpi_template = pd.ExcelFile(self.kpi_template_path)
        self.own_man_fk = OWN_DISTRIBUTOR_FK
        self.kpi_template = pd.ExcelFile(self.kpi_template_path)
        self.empty_prod_ids = self.all_products[
            self.all_products.product_name.str.contains(
                'empty', case=False)]['product_fk'].values
        self.irrelevant_prod_ids = self.all_products[
            self.all_products.product_name.str.contains(
                'irrelevant', case=False)]['product_fk'].values
        self.other_prod_ids = self.all_products[
            self.all_products.product_name.str.contains(
                'other', case=False)]['product_fk'].values

    def filter_and_send_kpi_to_calc(self):
        kpi_sheet = self.kpi_template.parse(KPI_NAMES_SHEET)
        kpi_sheet[KPI_FAMILY_COL] = kpi_sheet[KPI_FAMILY_COL].fillna(
            method='ffill')
        kpi_details = self.kpi_template.parse(KPI_DETAILS_SHEET)
        kpi_include_exclude = self.kpi_template.parse(KPI_INC_EXC_SHEET)
        for index, kpi_sheet_row in kpi_sheet.iterrows():
            if not is_nan(kpi_sheet_row[KPI_ACTIVE]):
                if str(kpi_sheet_row[KPI_ACTIVE]).strip().lower() in [
                        '0.0', 'n', 'no'
                ]:
                    Log.warning("KPI :{} deactivated in sheet.".format(
                        kpi_sheet_row[KPI_NAME_COL]))
                    continue
            kpi = self.kpi_static_data[
                (self.kpi_static_data[KPI_TYPE_COL] ==
                 kpi_sheet_row[KPI_NAME_COL])
                & (self.kpi_static_data['delete_time'].isnull())]
            if kpi.empty or kpi.session_relevance.values[0] == 1:
                Log.info(
                    "*** KPI Name:{name} not found in DB or is a SESSION LEVEL KPI for scene {scene} ***"
                    .format(name=kpi_sheet_row[KPI_NAME_COL],
                            scene=self.current_scene_fk))
                continue
            else:
                Log.info(
                    "KPI Name:{name} found in DB for scene {scene}".format(
                        name=kpi_sheet_row[KPI_NAME_COL],
                        scene=self.current_scene_fk))
                detail = kpi_details[kpi_details[KPI_NAME_COL] ==
                                     kpi[KPI_TYPE_COL].values[0]]
                # check for store types allowed
                permitted_store_types = [
                    x.strip().lower()
                    for x in detail[STORE_POLICY].values[0].split(',')
                    if x.strip()
                ]
                if self.store_info.store_type.iloc[0].lower(
                ) not in permitted_store_types:
                    if permitted_store_types and permitted_store_types[
                            0] != "all":
                        Log.warning(
                            "Not permitted store type - {type} for scene {scene}"
                            .format(type=kpi_sheet_row[KPI_NAME_COL],
                                    scene=self.current_scene_fk))
                        continue
                detail['pk'] = kpi['pk'].iloc[0]
                # gather details
                groupers, query_string = get_groupers_and_query_string(detail)
                kpi_include_exclude = kpi_include_exclude[
                    kpi_include_exclude.kpi_name != ASSORTMENTS]
                _include_exclude = kpi_include_exclude[
                    kpi_details[KPI_NAME_COL] == kpi[KPI_TYPE_COL].values[0]]
                # gather include exclude
                include_exclude_data_dict = get_include_exclude(
                    _include_exclude)
                dataframe_to_process = self.get_sanitized_match_prod_scene(
                    include_exclude_data_dict)
            # hack to cast all other than OWN_DISTRIBUTOR to non-sino
            non_sino_index = dataframe_to_process[
                OWN_CHECK_COL] != OWN_DISTRIBUTOR
            dataframe_to_process.loc[non_sino_index,
                                     OWN_CHECK_COL] = 'non-sino'
            if kpi_sheet_row[KPI_FAMILY_COL] in [FSOS, SIMON]:
                self.calculate_fsos(detail, groupers, query_string,
                                    dataframe_to_process)
            else:
                Log.error(
                    "From project: {proj}. Unexpected kpi_family: {type}. Please check."
                    .format(type=kpi_sheet_row[KPI_FAMILY_COL],
                            proj=self.project_name))
                pass
        return True

    def calculate_fsos(self, kpi, groupers, query_string,
                       dataframe_to_process):
        Log.info("Calculate {name} for scene {scene}".format(
            name=kpi.kpi_name.iloc[0], scene=self.current_scene_fk))
        if query_string:
            grouped_data_frame = dataframe_to_process.query(
                query_string).groupby(groupers)
        else:
            grouped_data_frame = dataframe_to_process.groupby(groupers)
        for group_id_tup, group_data in grouped_data_frame:
            if type(group_id_tup) not in [tuple, list]:
                # convert to a tuple
                group_id_tup = group_id_tup,
            param_id_map = dict(zip(groupers, group_id_tup))
            # the hack! This casts the value of distributor to that in DB as custom entity.
            if OWN_CHECK_COL in param_id_map:
                distributor_name = param_id_map.pop(OWN_CHECK_COL)
                if distributor_name == OWN_DISTRIBUTOR:
                    param_id_map[
                        OWN_CHECK_COL] = self.own_man_fk  # as per custom entity
                else:
                    param_id_map[
                        OWN_CHECK_COL] = OTHER_DISTRIBUTOR_FK  # as per custom entity
                param_id_map['manufacturer_fk'] = param_id_map[OWN_CHECK_COL]
            # SET THE numerator, denominator and context
            numerator_id = param_id_map.get(
                PARAM_DB_MAP[kpi['numerator'].iloc[0]]['key'])
            if numerator_id is None:
                raise Exception(
                    "Numerator cannot be null. Check SinoTH KPIToolBox [calculate_fsos]."
                )
            denominator_id = get_parameter_id(
                key_value=PARAM_DB_MAP[kpi['denominator'].iloc[0]]['key'],
                param_id_map=param_id_map)
            if denominator_id is None:
                # because 0 is good; check None specifically
                denominator_id = self.store_id
            context_id = get_parameter_id(
                key_value=PARAM_DB_MAP[kpi['context'].iloc[0]]['key'],
                param_id_map=param_id_map)
            if context_id is None:
                # because 0 is good
                context_id = self.store_id
            if PARAM_DB_MAP[kpi['denominator'].iloc[0]]['key'] == 'store_fk':
                denominator_df = dataframe_to_process
            else:
                denominator_df = dataframe_to_process.query(
                    '{key} == {value}'.format(
                        key=PARAM_DB_MAP[kpi['denominator'].iloc[0]]['key'],
                        value=denominator_id))
            if not len(denominator_df):
                Log.error(
                    "No denominator data for session {sess} and scene {scene} to calculate  {name}"
                    .format(sess=self.session_uid,
                            name=kpi.kpi_name.iloc[0],
                            scene=self.current_scene_fk))
                raise Exception(
                    "Denominator data cannot be null. Check SinoTH KPIToolBox [calculate_fsos]."
                )
            result = len(group_data) / float(len(denominator_df))
            # its the parent. Save the identifier result.
            self.common.write_to_db_result(
                fk=kpi['pk'].iloc[0],
                numerator_id=numerator_id,
                denominator_id=denominator_id,
                context_id=context_id,
                result=result,
                numerator_result=len(group_data),
                denominator_result=len(denominator_df),
                identifier_result="{}_{}_{}_{}".format(
                    kpi['kpi_name'].iloc[0],
                    kpi['pk'].iloc[0],
                    # numerator_id,
                    denominator_id,
                    context_id,
                ),
                should_enter=True,
                by_scene=True,
            )

        return True

    def get_sanitized_match_prod_scene(self, include_exclude_data_dict):
        scene_product_data = self.match_product_in_scene.merge(
            self.products,
            how='left',
            on=['product_fk'],
            suffixes=('', '_prod'))
        sanitized_products_in_scene = scene_product_data.merge(
            self.scene_template_info,
            how='left',
            on='scene_fk',
            suffixes=('', '_scene'))
        # flags
        include_empty = include_exclude_data_dict.get('empty')
        include_irrelevant = include_exclude_data_dict.get('irrelevant')
        include_others = include_exclude_data_dict.get('others')
        include_stacking = include_exclude_data_dict.get('stacking')
        # list
        scene_types_to_include = include_exclude_data_dict.get(
            'scene_types_to_include', False)
        categories_to_include = include_exclude_data_dict.get(
            'categories_to_include', False)
        brands_to_include = include_exclude_data_dict.get(
            'brands_to_include', False)
        ean_codes_to_include = include_exclude_data_dict.get(
            'ean_codes_to_include', False)
        # Start include items
        if scene_types_to_include:
            # list of scene types to include is present, otherwise all included
            Log.info("Include template/scene type {}".format(
                scene_types_to_include))
            sanitized_products_in_scene = sanitized_products_in_scene[
                sanitized_products_in_scene['template_name'].str.upper().isin([
                    x.upper() if type(x) in [unicode, str] else x
                    for x in scene_types_to_include
                ])]
        if not include_stacking:
            # exclude stacking if the flag is set
            Log.info(
                "Exclude stacking other than in layer 1 or negative stacking [menu]"
            )
            sanitized_products_in_scene = sanitized_products_in_scene.loc[
                sanitized_products_in_scene['stacking_layer'] <= 1]
        if categories_to_include:
            # list of categories to include is present, otherwise all included
            Log.info("Include categories {}".format(categories_to_include))
            sanitized_products_in_scene = sanitized_products_in_scene[
                sanitized_products_in_scene['category'].str.upper().isin([
                    x.upper() if type(x) in [unicode, str] else x
                    for x in categories_to_include
                ])]
        if brands_to_include:
            # list of brands to include is present, otherwise all included
            Log.info("Include brands {}".format(brands_to_include))
            sanitized_products_in_scene = sanitized_products_in_scene[
                sanitized_products_in_scene['brand_name'].str.upper().isin([
                    x.upper() if type(x) in [unicode, str] else x
                    for x in brands_to_include
                ])]
        if ean_codes_to_include:
            # list of ean_codes to include is present, otherwise all included
            Log.info("Include ean codes {}".format(ean_codes_to_include))
            sanitized_products_in_scene = sanitized_products_in_scene[
                sanitized_products_in_scene['product_ean_code'].str.upper(
                ).isin([
                    x.upper() if type(x) in [unicode, str] else x
                    for x in ean_codes_to_include
                ])]
        product_ids_to_exclude = []
        if not include_irrelevant:
            # add product ids to exclude with irrelevant
            product_ids_to_exclude.extend(self.irrelevant_prod_ids)
        if not include_others:
            # add product ids to exclude with others
            product_ids_to_exclude.extend(self.other_prod_ids)
        if not include_empty:
            # add product ids to exclude with empty
            product_ids_to_exclude.extend(self.empty_prod_ids)
        if product_ids_to_exclude:
            Log.info("Exclude product ids {}".format(product_ids_to_exclude))
            sanitized_products_in_scene.drop(sanitized_products_in_scene[
                sanitized_products_in_scene['product_fk'].isin(
                    product_ids_to_exclude)].index,
                                             inplace=True)
        return sanitized_products_in_scene
示例#10
0
class PillarsSceneToolBox:
    PROGRAM_TEMPLATE_PATH = os.path.join(
        os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'Data',
        'CCUS_Templatev7_February2020.xlsx')
    BITWISE_RECOGNIZER_SIZE = 6
    RECOGNIZED_BY_POS = BITWISE_RECOGNIZER_SIZE - 1
    RECOGNIZED_BY_SCENE_RECOGNITION = BITWISE_RECOGNIZER_SIZE - 2
    RECOGNIZED_BY_QURI = BITWISE_RECOGNIZER_SIZE - 3
    RECOGNIZED_BY_SURVEY = BITWISE_RECOGNIZER_SIZE - 4

    def __init__(self, data_provider, output, common):
        self.data_provider = data_provider
        self.common = common
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.templates = self.data_provider[Data.TEMPLATES]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        empties = self.all_products[self.all_products['product_type'] ==
                                    'Empty']['product_fk'].unique().tolist()
        self.match_product_in_scene = self.match_product_in_scene[~(
            self.match_product_in_scene['product_fk'].isin(empties))]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.template_fk = self.templates['template_fk'].iloc[0]
        self.scene_id = self.scene_info['scene_fk'][0]
        self.store_id = self.data_provider[Data.STORE_INFO]['store_fk'][0]
        # self.kpi_fk = self.common.get_kpi_fk_by_kpi_name(Const.POC)
        self.all_brand = self.all_products[[
            'brand_name', 'brand_fk'
        ]].drop_duplicates().set_index(u'brand_name')
        self.displays_in_scene = self.data_provider.match_display_in_scene
        self.ps_data_provider = PsDataProvider(self.data_provider, output)

        # bit-like sequence to symoblize recognizing methods. Each 'bit' symbolize a recognition method.
        self.bitwise_for_program_identifier_as_list = list(
            "0" * self.BITWISE_RECOGNIZER_SIZE)

    def is_scene_belong_to_program(self):
        # Get template (from file or from external targets)
        relevant_programs = self.get_programs(template=True)

        for i in xrange(len(relevant_programs)):

            # Get data for program from template:
            current_program_data = relevant_programs.iloc[i]
            program_name = current_program_data[
                Const.PROGRAM_NAME_FIELD]  # assumed to always be brand name!
            program_brand_name_fk = self.get_brand_fk_from_name(program_name)
            program_as_brand = current_program_data[
                Const.PROGRAM_NAME_BY_BRAND]
            program_as_brand_fk = self.get_brand_fk_from_name(program_as_brand)
            program_as_display_brand = current_program_data[
                Const.PROGRAM_NAME_BY_DISPLAY]
            program_as_template = current_program_data[
                Const.PROGRAM_NAME_BY_TEMPLATE]
            survey_question_for_program = current_program_data[
                Const.PROGRAM_NAME_BY_SURVEY_QUESTION]
            program_as_survey_answer = current_program_data[
                Const.PROGRAM_NAME_BY_SURVEY_ANSWER]
            score = 0

            # Checks if the scene was recognized as relevant program in one of possible recognition options:
            self.bitwise_for_program_identifier_as_list[self.RECOGNIZED_BY_POS] = \
                1 if self.found_program_products_by_brand(program_as_brand_fk) else 0

            self.bitwise_for_program_identifier_as_list[self.RECOGNIZED_BY_SCENE_RECOGNITION] = \
                1 if self.found_scene_program_by_display_brand(program_as_display_brand) else 0

            self.bitwise_for_program_identifier_as_list[self.RECOGNIZED_BY_QURI] = \
                1 if self.found_scene_program_by_quri(program_as_template) else 0

            self.bitwise_for_program_identifier_as_list[self.RECOGNIZED_BY_SURVEY] = \
                1 if self.found_scene_program_by_survey(survey_question_for_program, program_as_survey_answer) else 0

            # convert list of bits to a string in order to convert to decimal in results:
            bitwise_for_program_identifier_as_str = ''.join(
                map(str, self.bitwise_for_program_identifier_as_list))

            # convert string of binary-like to decimal value
            method_recognized_in_bitwise = int(
                bitwise_for_program_identifier_as_str, BINARY)

            score = 1 if method_recognized_in_bitwise > 0 else 0

            scene_kpi_fk = self.common.get_kpi_fk_by_kpi_name(
                kpi_name=Const.SCENE_KPI_NAME)
            self.common.write_to_db_result(fk=scene_kpi_fk,
                                           numerator_id=program_brand_name_fk,
                                           result=method_recognized_in_bitwise,
                                           score=score,
                                           by_scene=True,
                                           denominator_id=self.store_id)

    def get_programs(self, template=False):
        """
        This function gets the relevant programs from template/ external targets list.
        :param template: if True takes the template from Data folder in project.
                Else, takes the kpi's external targets.
        :return:
        """
        if template:
            programs = pd.read_excel(self.PROGRAM_TEMPLATE_PATH)
        else:
            programs = self.ps_data_provider.get_kpi_external_targets(
                ["Pillars Programs KPI"])

        if programs.empty:
            return programs

        # Get only relevant programs to check
        relevant_programs = programs.loc[
            (programs['start_date'].dt.date <= self.visit_date)
            & (programs['end_date'].dt.date >= self.visit_date)]

        return relevant_programs

    def get_brand_fk_from_name(self, brand_name):
        if pd.isnull(brand_name):
            return
        fk = self.all_brand.loc[brand_name]
        if not fk.empty:
            fk = fk.values[0]
        else:
            fk = None
        return fk

    def found_program_products_by_brand(self, brand_fk=None, brand_name=None):
        """
        This function can get brand either by fk or by name, with the assumption that in the
        'customer' template there is brand name and in the db targets there is pk.
        If none of the option was used, return False.
        Otherwise return if there were product in this scene with the brand given.
        """
        # checks if the scene's program was discovered by trax according to brand's recognized products
        if pd.isnull(brand_name) and pd.isnull(brand_fk):
            return False

        brand_id = 'brand_fk' if brand_fk else 'brand_name'
        brand_value = brand_fk if brand_fk else brand_name

        pos_products_in_brand = self.all_products[
            (self.all_products['product_type'] == 'POS')
            & (self.all_products[brand_id] == brand_value
               )]['product_fk'].unique().tolist()
        program_products_in_scene = self.match_product_in_scene[(
            self.match_product_in_scene['product_fk'].isin(
                pos_products_in_brand))]
        return len(program_products_in_scene) > 0

    def found_scene_program_by_quri(self, template_name):
        # checks if the scene's program was discovered by quri according to the template name

        if pd.isnull(template_name):
            return False

        if self.templates['template_fk'].empty:
            return False

        return template_name in self.templates['template_name'].values

    def found_scene_program_by_survey(
            self, survey_question,
            survey_answer):  # TODO- complete after survey created

        # Cannot complete until found relevant tables

        if pd.isnull(survey_question) or pd.isnull(survey_answer):
            return False

        pass

    def found_scene_program_by_display_brand(self, brand_name):
        # checks if the scene's program was discovered by scene recognition

        if pd.isnull(brand_name):
            return False

        if self.data_provider.match_display_in_scene.empty:
            return False
        display_in_brand = len(self.data_provider.match_display_in_scene.loc[
            self.data_provider.match_display_in_scene['display_brand_name'] ==
            brand_name])

        return display_in_brand > 0
示例#11
0
class MenuToolBox(GlobalSessionToolBox):

    def __init__(self, data_provider, common):
        GlobalSessionToolBox.__init__(self, data_provider, None)
        # self.matches = self.get_filtered_matches()
        self.store_number = self.store_info.store_number_1.iloc[0]
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalcAdmin)
        self.custom_entity = self.get_custom_entity()

        self.targets = self.ps_data_provider.get_kpi_external_targets(kpi_fks=[6006],
                                                                      key_fields=['store_number_1', 'product_fk'],
                                                                      key_filters={'store_number_1': self.store_number})
        self.common = common

    def main_calculation(self):
        """This method calculates the entire Menu Brand KPIs set."""
        self.menu_count()

    def menu_count(self):
        kpi_fk = self.get_kpi_fk_by_kpi_type(Consts.MENU_KPI_CHILD)
        parent_kpi = self.get_kpi_fk_by_kpi_type(Consts.TOTAL_MENU_KPI_SCORE)
        # we need to save a second set of KPIs with heirarchy for the mobile report
        kpi_fk_mr = self.get_kpi_fk_by_kpi_type(Consts.MENU_KPI_CHILD_MR)
        parent_kpi_mr = self.get_kpi_fk_by_kpi_type(Consts.TOTAL_MENU_KPI_SCORE_MR)

        if self.targets.empty:
            return
        try:
            menu_product_fks = [t for t in self.targets.product_fk.unique().tolist() if pd.notna(t)]
        except AttributeError:
            Log.warning('Menu Count targets are corrupt for this store')
            return

        filtered_scif = self.scif[self.scif['template_group'].str.contains('Menu')]
        present_menu_scif_sub_brands = filtered_scif.sub_brand.unique().tolist()
        passed_products = 0

        for product_fk in menu_product_fks:
            result = 0
            sub_brand = self.all_products['sub_brand'][self.all_products['product_fk'] == product_fk].iloc[0]

            custom_entity_df = self.custom_entity['pk'][self.custom_entity['name'] == sub_brand]
            if custom_entity_df.empty:
                custom_entity_pk = -1
            else:
                custom_entity_pk = custom_entity_df.iloc[0]

            if sub_brand in present_menu_scif_sub_brands:
                result = 1
                passed_products += 1

            self.write_to_db(fk=kpi_fk_mr, numerator_id=product_fk, numerator_result=0, denominator_result=0,
                             denominator_id=custom_entity_pk,
                             result=result, score=0, identifier_parent=parent_kpi_mr, identifier_result=kpi_fk_mr,
                             should_enter=True)

            self.write_to_db(fk=kpi_fk, numerator_id=product_fk, numerator_result=0, denominator_result=0,
                             denominator_id=custom_entity_pk, result=result, score=0)

        target_products = len(menu_product_fks)
        self.write_to_db(fk=parent_kpi_mr, numerator_id=self.manufacturer_fk, numerator_result=0, denominator_result=0,
                         denominator_id=self.store_id,
                         result=passed_products, score=0, target=target_products, identifier_result=parent_kpi_mr)

        self.write_to_db(fk=parent_kpi, numerator_id=self.manufacturer_fk, numerator_result=0, denominator_result=0,
                         denominator_id=self.store_id,
                         result=passed_products, score=0, target=target_products)

    def get_custom_entity(self):
        query = DiageoQueries.get_custom_entities_query()
        query_result = pd.read_sql_query(query, self.rds_conn.db)
        return query_result
示例#12
0
class JRIJPToolBox:
    LEVEL1 = 1
    LEVEL2 = 2
    LEVEL3 = 3

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = Common(self.data_provider)
        self.match_display_in_scene = self.data_provider.match_display_in_scene
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.templates = self.data_provider[Data.TEMPLATES]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.kpi_results_queries = []
        self.templates_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'Data')
        self.excel_file_path = os.path.join(self.templates_path, 'Template.xlsx')
        self.external_targets = self.ps_data_provider.get_kpi_external_targets(
            kpi_operation_types=["Target Config"],
            key_fields=["product_fks", "template_fks", "product_group_fk"],
            data_fields=["stacking_exclude", "min_product_facing", "best_shelf_position", "group_facings_count"]
            )

    def main_calculation(self, *args, **kwargs):
        """
        This function calculates the KPI results.
        Important:
            The name of the KPI is used to name the function to calculate it.
            if kpi_name is *test_calc*; the function will be *calculate_test_calc*
        """
        self.calculate_config_related()
        self.parse_and_send_kpi_to_calc()
        self.common.commit_results_data()
        return

    def calculate_config_related(self):
        if self.external_targets.empty:
            Log.info("Not calculating Config related KPIs for Canvas."
                     "External Targets empty while running session: {}".format(self.session_uid))
            return True
        product_presence_from_target_pk = self.kpi_static_data[
            (self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
            & (self.kpi_static_data[TYPE] == PRODUCT_PRESENCE_FROM_TARGET)
            & (self.kpi_static_data['delete_time'].isnull())].iloc[0].pk
        product_position_from_target_pk = self.kpi_static_data[
            (self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
            & (self.kpi_static_data[TYPE] == PRODUCT_POSITION_FROM_TARGET)
            & (self.kpi_static_data['delete_time'].isnull())].iloc[0].pk
        product_facing_from_target_pk = self.kpi_static_data[
            (self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
            & (self.kpi_static_data[TYPE] == PRODUCT_FACING_FROM_TARGET)
            & (self.kpi_static_data['delete_time'].isnull())].iloc[0].pk
        overall_result_from_target_pk = self.kpi_static_data[
            (self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
            & (self.kpi_static_data[TYPE] == OVERALL_RESULT_FROM_TARGET)
            & (self.kpi_static_data['delete_time'].isnull())].iloc[0].pk
        overall_score_from_target_pk = self.kpi_static_data[
            (self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
            & (self.kpi_static_data[TYPE] == OVERALL_SCORE_FROM_TARGET)
            & (self.kpi_static_data['delete_time'].isnull())].iloc[0].pk
        match_prod_in_scene_data = self.match_product_in_scene \
            .merge(self.scene_info, on='scene_fk', suffixes=('', '_scene')) \
            .merge(self.templates, on='template_fk', suffixes=('', '_template'))
        self.external_targets.fillna('', inplace=True)
        for index, each_target in self.external_targets.iterrows():
            _each_target_dict = each_target.to_dict()
            group_fk = _each_target_dict.get('product_group_fk')
            product_fks = _each_target_dict.get('product_fks')
            if type(product_fks) != list:
                product_fks = [product_fks]
            best_shelf_position = _each_target_dict.get('best_shelf_position')
            if type(best_shelf_position) != list:
                best_shelf_position = [best_shelf_position]
            min_product_facing = _each_target_dict.get('min_product_facing', 0)
            template_fks = _each_target_dict.get('template_fks')
            if template_fks and type(template_fks) != list:
                template_fks = [template_fks]
            stacking_exclude = _each_target_dict.get('stacking_exclude')
            min_group_product_facing = _each_target_dict.get('group_facings_count', 0)
            # get mpis based on details
            filtered_mpis = match_prod_in_scene_data
            if template_fks:
                filtered_mpis = filtered_mpis[match_prod_in_scene_data['template_fk'].isin(template_fks)]
            if stacking_exclude == '1':
                filtered_mpis = filtered_mpis[match_prod_in_scene_data['stacking_layer'] == 1]
            product_presence_data = self.calculate_product_presence(
                kpi_pk=product_presence_from_target_pk,
                filtere_mpis=filtered_mpis,
                group_fk=group_fk,
                product_fks=product_fks,
                min_product_facing=min_product_facing
            )
            product_position_data = self.calculate_product_position(
                kpi_pk=product_position_from_target_pk,
                filtere_mpis=filtered_mpis,
                group_fk=group_fk,
                product_fks=product_fks,
                best_shelf_position=best_shelf_position
            )
            product_facings_data = self.calculate_product_facings(
                kpi_pk=product_facing_from_target_pk,
                filtere_mpis=filtered_mpis,
                group_fk=group_fk,
                product_fks=product_fks
            )
            self.calculate_overall_result_and_score(result_kpi_pk=overall_result_from_target_pk,
                                                    score_kpi_fk=overall_score_from_target_pk,
                                                    group_fk=group_fk,
                                                    product_presence_data=product_presence_data,
                                                    product_position_data=product_position_data,
                                                    product_facings_data=product_facings_data,
                                                    best_shelf_position=best_shelf_position,
                                                    min_group_product_facing=min_group_product_facing
                                                    )
        pass

    def calculate_product_presence(self, kpi_pk, filtere_mpis, group_fk, product_fks, min_product_facing):
        data = {}
        for each_product in product_fks:
            prod_data_in_mpis = filtere_mpis[filtere_mpis['product_fk'] == each_product]
            result = 0
            if len(prod_data_in_mpis) >= int(min_product_facing):
                result = 1
            Log.info("Saving product presence for product: {product} as {result} in session {sess} in group: {group}"
                     .format(product=each_product,
                             result=result,
                             sess=self.session_uid,
                             group=group_fk
                             ))
            data[each_product] = result
            self.common.write_to_db_result(fk=kpi_pk,
                                           numerator_id=group_fk,
                                           denominator_id=each_product,
                                           context_id=self.all_products[self.all_products['product_fk']
                                                                        ==each_product].category_fk.iloc[0],
                                           result=result,
                                           score=result,
                                           )
        return data

    def calculate_product_position(self, kpi_pk, filtere_mpis, group_fk, product_fks, best_shelf_position):
        data = {}
        for each_product in product_fks:
            prod_data_in_mpis = filtere_mpis[filtere_mpis['product_fk'] == each_product]
            result = 0  # best shelf from top
            score = 0  # best shelf position from top in CONFIG?
            if prod_data_in_mpis.empty:
                Log.info("Position KPI => Product: {} not found in session: {} for group: {}".format(
                    each_product, self.session_uid, group_fk
                ))
                result = 0
                score = 0
            else:
                prod_data_in_mpis_sorted = prod_data_in_mpis.sort_values(by=['shelf_number'])
                result = prod_data_in_mpis_sorted.iloc[0]['shelf_number']  # => presence_lowest_shelf
                if result in [int(x) for x in best_shelf_position if x.strip()]:
                    score = 1

            Log.info("Saving product position for product: {product} as"
                     " lowest={result}/is in config={score} in session"
                     " {sess} in group: {group}"
                     .format(product=each_product,
                             result=result,
                             score=score,
                             sess=self.session_uid,
                             group=group_fk
                             ))
            data[each_product] = (result, score)
            self.common.write_to_db_result(fk=kpi_pk,
                                           numerator_id=group_fk,
                                           denominator_id=each_product,
                                           context_id=self.all_products[self.all_products['product_fk']
                                                                        ==each_product].category_fk.iloc[0],
                                           result=result,
                                           score=score,
                                           )
        return data

    def calculate_product_facings(self, kpi_pk, filtere_mpis, group_fk, product_fks):
        data = {}
        for each_product in product_fks:
            prod_data_in_mpis = filtere_mpis[filtere_mpis['product_fk'] == each_product]
            if prod_data_in_mpis.empty:
                Log.info("Facings KPI => Product: {} not found in session: {} for group: {}".format(
                    each_product, self.session_uid, group_fk
                ))
                result = 0
            else:
                result = len(prod_data_in_mpis)
            Log.info("Saving product facings for product: {product} as {result} in session {sess} in group: {group}"
                     .format(product=each_product,
                             result=result,
                             sess=self.session_uid,
                             group=group_fk
                             ))
            data[each_product] = result
            self.common.write_to_db_result(fk=kpi_pk,
                                           numerator_id=group_fk,
                                           denominator_id=each_product,
                                           context_id=self.all_products[self.all_products['product_fk']
                                                                        ==each_product].category_fk.iloc[0],
                                           result=result,
                                           score=result,
                                           )
        return data

    def calculate_overall_result_and_score(self, result_kpi_pk, score_kpi_fk, group_fk,
                                           product_presence_data, product_position_data,
                                           product_facings_data, best_shelf_position,
                                           min_group_product_facing):
        numerator_result = 1 in product_presence_data.values()
        # product_position_data -> (min_shelf, is_in_config)
        min_level_of_product = 0
        if product_position_data:
            _score_products_presence = filter(lambda x: x[1] == 1, product_position_data.values())
            if _score_products_presence:
                # find min result among the in config ones
                _score_products_presence.sort(key=lambda x: x[0])
                min_level_of_product = int(_score_products_presence[0][0])
            else:
                # score is all 0
                _score_products_presence_present = filter(lambda x: x[0] != 0, product_position_data.values())
                _score_products_presence_present.sort(key=lambda x: x[0])
                if _score_products_presence_present:
                    min_level_of_product = _score_products_presence_present[0][0]
        else:
            Log.info("Product presence information is empty for any product in group = {group_fk}".format(
                group_fk=group_fk
            ))
        Log.info("Saving Overall Result for group: {group_fk} in session {sess}"
                 .format(group_fk=group_fk,
                         sess=self.session_uid,
                         ))
        self.common.write_to_db_result(fk=result_kpi_pk,
                                       numerator_id=group_fk,
                                       denominator_id=self.store_id,
                                       context_id=self.store_id,
                                       numerator_result=int(numerator_result),  # bool to int
                                       denominator_result=min_level_of_product,
                                       result=sum(product_facings_data.values()),  # bool to int
                                       )
        Log.info("Saving Overall Score for group: {group_fk} in session {sess}"
                 .format(group_fk=group_fk,
                         sess=self.session_uid,
                         ))
        is_in_best_shelf = False
        if best_shelf_position and min_level_of_product:
            if type(best_shelf_position) != list:
                best_shelf_position = [best_shelf_position]
            is_in_best_shelf = min_level_of_product in map(lambda x: int(x), best_shelf_position)
        has_minumum_facings_per_config = False
        if not min_group_product_facing or not min_group_product_facing.strip():
            min_group_product_facing = 0
        if sum(product_facings_data.values()) >= int(min_group_product_facing):
            has_minumum_facings_per_config = True
        self.common.write_to_db_result(fk=score_kpi_fk,
                                       numerator_id=group_fk,
                                       denominator_id=self.store_id,
                                       context_id=self.store_id,
                                       numerator_result=int(numerator_result),  # bool to int
                                       denominator_result=int(is_in_best_shelf),  # bool to int
                                       result=int(has_minumum_facings_per_config),  # bool to int
                                       score=int(all([numerator_result, is_in_best_shelf,
                                                      has_minumum_facings_per_config]))   # bool to int
                                       )

    def parse_and_send_kpi_to_calc(self):
        kpi_sheet = self.get_template_details(KPI_SHEET_NAME)
        for index, kpi_sheet_row in kpi_sheet.iterrows():
            kpi = self.kpi_static_data[(self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
                                       & (self.kpi_static_data[TYPE] == kpi_sheet_row[KPI_TYPE])
                                       & (self.kpi_static_data['delete_time'].isnull())]
            if kpi.empty:
                Log.info("KPI Name:{} not found in DB".format(kpi_sheet_row[KPI_NAME]))
            else:
                Log.info("KPI Name:{} found in DB".format(kpi_sheet_row[KPI_NAME]))
                kpi_method_to_calc = getattr(self, 'calculate_{kpi}'.format(kpi=kpi_sheet_row[KPI_NAME].lower()), None)
                if not kpi_method_to_calc:
                    Log.warning("Method not defined for KPI Name:{}.".format(kpi_sheet_row[KPI_NAME].lower()))
                    pass
                kpi_fk = kpi.pk.values[0]
                kpi_method_to_calc(kpi_fk)

    def calculate_count_posm_per_scene(self, kpi_fk):
        if self.match_display_in_scene.empty:
            Log.info("No POSM detected at scene level for session: {}".format(self.session_uid))
            return False
        grouped_data = self.match_display_in_scene.groupby(['scene_fk', 'display_fk'])
        for data_tup, scene_data_df in grouped_data:
            scene_fk, display_fk = data_tup
            posm_count = len(scene_data_df)
            template_fk = self.scene_info[self.scene_info['scene_fk'] == scene_fk].get('template_fk')
            if not template_fk.empty:
                cur_template_fk = int(template_fk)
            else:
                Log.info("JRIJP: Scene ID {scene} is not complete and not found in scene Info.".format(
                    scene=scene_fk))
                continue
            self.common.write_to_db_result(fk=kpi_fk,
                                           numerator_id=display_fk,
                                           denominator_id=self.store_id,
                                           context_id=cur_template_fk,
                                           result=posm_count,
                                           score=scene_fk)

    def calculate_facings_in_cell_per_product(self, kpi_fk):
        match_prod_scene_data = self.match_product_in_scene.merge(
            self.products, how='left', on='product_fk', suffixes=('', '_prod'))
        grouped_data = match_prod_scene_data.query(
            '(stacking_layer==1) or (product_type=="POS")'
        ).groupby(
            ['scene_fk', 'bay_number', 'shelf_number', 'product_fk']
        )
        for data_tup, scene_data_df in grouped_data:
            scene_fk, bay_number, shelf_number, product_fk = data_tup
            facings_count_in_cell = len(scene_data_df)
            cur_template_fk = int(self.scene_info[self.scene_info['scene_fk'] == scene_fk].get('template_fk'))
            self.common.write_to_db_result(fk=kpi_fk,
                                           numerator_id=product_fk,
                                           denominator_id=self.store_id,
                                           context_id=cur_template_fk,
                                           numerator_result=bay_number,
                                           denominator_result=shelf_number,
                                           result=facings_count_in_cell,
                                           score=scene_fk)

    def get_template_details(self, sheet_name):
        template = pd.read_excel(self.excel_file_path, sheetname=sheet_name)
        return template
class SceneToolBox(GlobalSceneToolBox):

    def __init__(self, data_provider, output):
        GlobalSceneToolBox.__init__(self, data_provider, output)
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.targets = self.ps_data_provider.get_kpi_external_targets(key_fields=['KPI Type'], data_fields=[
                                                                                   'Location: JSON',
                                                                                   'Config Params: JSON',
                                                                                   'Dataset 1: JSON'])
        self.gold_zone_scene_location_kpi = ['Lobby/Entrance', 'Main Alley/Hot Zone', 'Gold Zone End Cap',
                                             'Lobby/Main Entrance']

    def main_function(self):
        self.calculate_scene_location()
        return

    def calculate_scene_location(self):
        scene_location_kpi_template = self.targets[self.targets[Consts.KPI_TYPE].isin(['Scene Location'])]
        for i, row in scene_location_kpi_template.iterrows():
            row = self.apply_json_parser(row)
            return_holder = self._get_kpi_name_and_fk(row)
            relevant_scif = self._parse_json_filters_to_df(row)
            if relevant_scif.empty:
                return
            scene_store_area_df = self.get_store_area_df()
            scene_store_area_df = scene_store_area_df[scene_store_area_df.scene_fk.isin(relevant_scif.scene_fk.unique())]
            # scene_store_area_df['result'] = scene_store_area_df.name.apply(
            #     lambda x: 1 if x in self.gold_zone_scene_location_kpi else 0)
            scene_store_area_df['result'] = np.in1d(scene_store_area_df.name.values,
                                                    self.gold_zone_scene_location_kpi) * 1

            for store_area_row in scene_store_area_df.itertuples():

                self.common.write_to_db_result(fk=return_holder[1], numerator_id=store_area_row.pk,
                                               numerator_result=store_area_row.result, result=store_area_row.result,
                                               denominator_id=self.store_id, denominator_result=1,
                                               should_enter=True, by_scene=True)

    def apply_json_parser(self, row):
        json_relevent_rows_with_parse_logic = row[row.index.str.contains('JSON')].apply(self.parse_json_row)
        row = row[~ row.index.isin(json_relevent_rows_with_parse_logic.index)].append(
            json_relevent_rows_with_parse_logic)
        return row

    def parse_json_row(self, item):
        '''
        :param item: improper json value (formatted incorrectly)
        :return: properly formatted json dictionary

        The function will be in conjunction with apply. The function will applied on the row(pandas series). This is
            meant to convert the json comprised of improper format of strings and lists to a proper dictionary value.
        '''
        if item:
            container = self.prereq_parse_json_row(item)
        else:
            container = None
        return container

    @staticmethod
    def prereq_parse_json_row(item):
        '''
        primarly logic for formatting the value of the json
        '''

        if isinstance(item, list):
            container = OrderedDict()
            for it in item:
                # value = re.findall("[0-9a-zA-Z_]+", it)
                value = re.findall("'([^']*)'", it)
                if len(value) == 2:
                    for i in range(0, len(value), 2):
                        container[value[i]] = [value[i + 1]]
                else:
                    if len(container.items()) == 0:
                        print('issue')  # delete later
                        # raise error
                        # haven't encountered an this. So should raise an issue.
                        pass
                    else:
                        last_inserted_value_key = container.items()[-1][0]
                        container.get(last_inserted_value_key).append(value[0])
        else:
            container = eval(item)

        return container

    def get_store_area_df(self):
        query = """
                 select st.pk, sst.scene_fk, st.name, sc.session_uid 
                 from probedata.scene_store_task_area_group_items sst
                 join static.store_task_area_group_items st on st.pk=sst.store_task_area_group_item_fk
                 join probedata.scene sc on sc.pk=sst.scene_fk
                 where sc.delete_time is null and sc.session_uid = '{}' and sst.scene_fk = '{}';
                 """.format(self.session_uid, self.scene_info.scene_fk.iat[0])

        df = pd.read_sql_query(query, self.rds_conn.db)
        return df

    def _get_kpi_name_and_fk(self, row):
        kpi_name = row[Consts.KPI_NAME]
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name)
        output = [kpi_name, kpi_fk]
        return output

    def _parse_json_filters_to_df(self, row):
        JSON = row[row.index.str.contains('JSON') & (~ row.index.str.contains('Config Params'))]
        filter_JSON = JSON[~JSON.isnull()]

        filtered_scif = self.scif
        for each_JSON in filter_JSON:
            final_JSON = {'population': each_JSON} if ('include' or 'exclude') in each_JSON else each_JSON
            filtered_scif = ParseInputKPI.filter_df(final_JSON, filtered_scif)
        if 'include_stacking' in row['Config Params: JSON'].keys():
            including_stacking = row['Config Params: JSON']['include_stacking'][0]
            filtered_scif[
                Consts.FINAL_FACINGS] = filtered_scif.facings if including_stacking == 'True' else filtered_scif.facings_ign_stack
            filtered_scif = filtered_scif[filtered_scif.stacking_layer == 1]
        return filtered_scif
示例#14
0
class StraussfritolayilUtil(UnifiedKPISingleton):
    def __init__(self, output, data_provider):
        super(StraussfritolayilUtil, self).__init__(data_provider)
        self.output = output
        self.common = Common(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.ps_data = PsDataProvider(self.data_provider, self.output)
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.brand_mix_df = self.get_brand_mix_df()
        self.add_sub_brand_to_scif()
        self.add_brand_mix_to_scif()
        self.match_probe_in_scene = self.ps_data.get_product_special_attribute_data(self.session_uid)
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        if not self.match_product_in_scene.empty:
            self.match_product_in_scene = self.match_product_in_scene.merge(self.scif[Consts.RELEVENT_FIELDS],
                                                                            on=["scene_fk", "product_fk"], how="left")
            self.filter_scif_and_mpis_to_contain_only_primary_shelf()
        else:
            unique_fields = [ele for ele in Consts.RELEVENT_FIELDS if ele not in ["product_fk", "scene_fk"]]
            self.match_product_in_scene = pd.concat([self.match_product_in_scene,
                                                     pd.DataFrame(columns=unique_fields)], axis=1)
        self.match_product_in_scene_wo_hangers = self.exclude_special_attribute_products(df=self.match_product_in_scene)
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.additional_attribute_2 = self.store_info[Consts.ADDITIONAL_ATTRIBUTE_2].values[0]
        self.additional_attribute_3 = self.store_info[Consts.ADDITIONAL_ATTRIBUTE_3].values[0]
        self.additional_attribute_4 = self.store_info[Consts.ADDITIONAL_ATTRIBUTE_4].values[0]
        self.store_id = self.store_info['store_fk'].values[0] if self.store_info['store_fk'] is not None else 0
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.toolbox = GENERALToolBox(self.data_provider)
        self.kpi_external_targets = self.ps_data.get_kpi_external_targets(key_fields=Consts.KEY_FIELDS,
                                                                          data_fields=Consts.DATA_FIELDS)
        self.filter_external_targets()
        self.assortment = Assortment(self.data_provider, self.output)
        self.lvl3_assortment = self.set_updated_assortment()
        self.own_manuf_fk = int(self.data_provider.own_manufacturer.param_value.values[0])
        self.own_manufacturer_matches_wo_hangers = self.match_product_in_scene_wo_hangers[
            self.match_product_in_scene_wo_hangers['manufacturer_fk'] == self.own_manuf_fk]

    def set_updated_assortment(self):
        assortment_result = self.assortment.get_lvl3_relevant_ass()
        if assortment_result.empty:
            return pd.DataFrame(columns=["kpi_fk_lvl2", "kpi_fk_lvl3"])
        assortment_result = self.calculate_lvl3_assortment(assortment_result)
        replacement_eans_df = pd.DataFrame([json_normalize(json.loads(js)).values[0] for js
                                            in assortment_result['additional_attributes']])
        replacement_eans_df.columns = [Consts.REPLACMENT_EAN_CODES]
        replacement_eans_df = replacement_eans_df[Consts.REPLACMENT_EAN_CODES].apply(lambda row: [
            x.strip() for x in str(row).split(",")] if row else None)
        assortment_result = assortment_result.join(replacement_eans_df)
        assortment_result['facings_all_products'] = assortment_result['facings'].copy()
        assortment_result['facings_all_products_wo_hangers'] = assortment_result['facings_wo_hangers'].copy()
        assortment_result = self.handle_replacment_products_row(assortment_result)
        return assortment_result

    def filter_external_targets(self):
        self.kpi_external_targets = self.kpi_external_targets[
            (self.kpi_external_targets[Consts.ADDITIONAL_ATTRIBUTE_2].str.encode("utf8").isin(
                [None, self.additional_attribute_2.encode("utf8")])) &
            (self.kpi_external_targets[Consts.ADDITIONAL_ATTRIBUTE_3].str.encode("utf8").isin(
                [None, self.additional_attribute_3.encode("utf8")])) &
            (self.kpi_external_targets[Consts.ADDITIONAL_ATTRIBUTE_4].str.encode("utf8").isin(
                [None, self.additional_attribute_4.encode("utf8")]))]

    def calculate_lvl3_assortment(self, assortment_result):
        """
        :return: data frame on the sku level with the following fields:
        ['assortment_group_fk', 'assortment_fk', 'target', 'product_fk', 'in_store', 'kpi_fk_lvl1',
        'kpi_fk_lvl2', 'kpi_fk_lvl3', 'group_target_date', 'assortment_super_group_fk',
         'super_group_target', 'additional_attributes']. Indicates whether the product was in the store (1) or not (0).
        """
        if assortment_result.empty:
            return assortment_result
        assortment_result['in_store_wo_hangers'] = assortment_result['in_store'].copy()
        products_in_session = list(self.match_product_in_scene['product_fk'].values)
        products_in_session_wo_hangers = list(self.match_product_in_scene_wo_hangers['product_fk'].values)
        assortment_result.loc[assortment_result['product_fk'].isin(products_in_session), 'in_store'] = 1
        assortment_result.loc[assortment_result['product_fk'].isin(products_in_session_wo_hangers),
                              'in_store_wo_hangers'] = 1
        assortment_result['facings'] = 0
        assortment_result['facings_wo_hangers'] = 0
        product_assort = assortment_result['product_fk'].unique()
        for sku in product_assort:
            assortment_result.loc[assortment_result['product_fk'] == sku, 'facings'] = \
                len(self.match_product_in_scene[self.match_product_in_scene['product_fk'] == sku])
            assortment_result.loc[assortment_result['product_fk'] == sku, 'facings_wo_hangers'] = \
                len(self.match_product_in_scene_wo_hangers[self.match_product_in_scene_wo_hangers['product_fk'] == sku])
        return assortment_result

    def handle_replacment_products_row(self, assortment_result):
        additional_products_df = assortment_result[~assortment_result[Consts.REPLACMENT_EAN_CODES].isnull()]
        products_in_session = set(self.match_product_in_scene['product_ean_code'].values)
        products_in_session_wo_hangers = set(self.match_product_in_scene_wo_hangers['product_ean_code'].values)
        for i, row in additional_products_df.iterrows():
            replacement_products = row[Consts.REPLACMENT_EAN_CODES]
            facings = len(self.match_product_in_scene[self.match_product_in_scene[
                'product_ean_code'].isin(replacement_products)])
            facings_wo_hangers = len(self.match_product_in_scene_wo_hangers[self.match_product_in_scene_wo_hangers[
                'product_ean_code'].isin(replacement_products)])
            assortment_result.loc[i, 'facings_all_products'] = facings + row['facings']
            assortment_result.loc[i, 'facings_all_products_wo_hangers'] = facings_wo_hangers + row['facings_wo_hangers']
            if row['in_store'] != 1:
                for sku in replacement_products:
                    if sku in products_in_session:
                        product_df = self.all_products[self.all_products['product_ean_code'] == sku]['product_fk']
                        assortment_result.loc[i, 'product_fk'] = product_df.values[0]
                        assortment_result.loc[i, 'in_store'] = 1
                        break
            if row['in_store_wo_hangers'] != 1:
                for sku in replacement_products:
                    if sku in products_in_session_wo_hangers:
                        product_df = self.all_products[self.all_products['product_ean_code'] == sku]['product_fk']
                        assortment_result.loc[i, 'product_fk'] = product_df.values[0]
                        assortment_result.loc[i, 'in_store_wo_hangers'] = 1
                        break
        return assortment_result

    def filter_scif_and_mpis_to_contain_only_primary_shelf(self):
        self.scif = self.scif[self.scif.location_type == Consts.PRIMARY_SHELF]
        self.match_product_in_scene = self.match_product_in_scene[self.match_product_in_scene.location_type ==
                                                                  Consts.PRIMARY_SHELF]

    def add_sub_brand_to_scif(self):
        sub_brand_df = self.ps_data.get_custom_entities_df(entity_type_name='Sub_Brand_Local')
        sub_brand_df = sub_brand_df[['entity_name', 'entity_fk']].copy()
        # sub_brand_df['entity_name'] = sub_brand_df['entity_name'].str.lower()
        sub_brand_df.rename({'entity_fk': 'sub_brand_fk'}, axis='columns', inplace=True)
        # delete duplicates by name and entity_type_fk to avoid recognition duplicates.
        sub_brand_df.drop_duplicates(subset=['entity_name'], keep='first', inplace=True)
        self.scif['Sub_Brand_Local'] = self.scif['Sub_Brand_Local'].fillna('no value')
        self.scif = self.scif.merge(sub_brand_df, left_on='Sub_Brand_Local', right_on="entity_name", how="left")
        self.scif['sub_brand_fk'].fillna(Consts.SUB_BRAND_NO_VALUE, inplace=True)

    def get_brand_mix_df(self):
        brand_mix_df = self.ps_data.get_custom_entities_df(entity_type_name='Brand_Mix')
        brand_mix_df = brand_mix_df[['entity_name', 'entity_fk']].copy()
        brand_mix_df.rename({'entity_fk': 'brand_mix_fk'}, axis='columns', inplace=True)
        # delete duplicates by name and entity_type_fk to avoid recognition duplicates.
        brand_mix_df.drop_duplicates(subset=['entity_name'], keep='first', inplace=True)
        return brand_mix_df

    def add_brand_mix_to_scif(self):

        self.scif['Brand_Mix'] = self.scif['Brand_Mix'].fillna('no value')
        self.scif = self.scif.merge(self.brand_mix_df, left_on='Brand_Mix', right_on="entity_name", how="left")
        self.scif['brand_mix_fk'].fillna(Consts.BRAND_MIX_NO_VALUE, inplace=True)

    @staticmethod
    def calculate_sos_result(numerator, denominator):
        if denominator == 0:
            return 0
        result = 100 * round((numerator / float(denominator)), 3)
        return result

    def exclude_special_attribute_products(self, df):
        """
        Helper to exclude smart_attribute products
        :return: filtered df without smart_attribute products
        """
        if self.match_probe_in_scene.empty:
            return df
        smart_attribute_df = self.match_probe_in_scene[self.match_probe_in_scene['name'] == Consts.ADDITIONAL_DISPLAY]
        if smart_attribute_df.empty:
            return df
        match_product_in_probe_fks = smart_attribute_df['match_product_in_probe_fk'].tolist()
        df = df[~df['probe_match_fk'].isin(match_product_in_probe_fks)]
        return df
示例#15
0
class HEINZCRToolBox:
    LVL3_HEADERS = ['assortment_group_fk', 'assortment_fk', 'target', 'product_fk',
                    'in_store', 'kpi_fk_lvl1', 'kpi_fk_lvl2', 'kpi_fk_lvl3', 'group_target_date',
                    'assortment_super_group_fk']
    LVL2_HEADERS = ['assortment_group_fk', 'assortment_fk', 'target', 'passes', 'total',
                    'kpi_fk_lvl1', 'kpi_fk_lvl2', 'group_target_date']
    LVL1_HEADERS = ['assortment_group_fk', 'target', 'passes', 'total', 'kpi_fk_lvl1']
    ASSORTMENT_FK = 'assortment_fk'
    ASSORTMENT_GROUP_FK = 'assortment_group_fk'
    ASSORTMENT_SUPER_GROUP_FK = 'assortment_super_group_fk'
    BRAND_VARIENT = 'brand_varient'
    NUMERATOR = 'numerator'
    DENOMINATOR = 'denominator'
    DISTRIBUTION_KPI = 'Distribution - SKU'
    OOS_SKU_KPI = 'OOS - SKU'
    OOS_KPI = 'OOS'

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = CommonV2  # remove later
        self.common_v2 = CommonV2(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.rds_conn = PSProjectConnector(self.project_name, DbUsers.CalculationEng)
        self.kpi_results_queries = []
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.survey = Survey(self.data_provider, output=self.output, ps_data_provider=self.ps_data_provider,
                             common=self.common_v2)
        self.store_sos_policies = self.ps_data_provider.get_store_policies()
        self.labels = self.ps_data_provider.get_labels()
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_info = self.ps_data_provider.get_ps_store_info(self.store_info)
        self.country = self.store_info['country'].iloc[0]
        self.current_date = datetime.now()
        self.extra_spaces_template = pd.read_excel(Const.EXTRA_SPACES_RELEVANT_SUB_CATEGORIES_PATH)
        self.store_targets = pd.read_excel(Const.STORE_TARGETS_PATH)
        self.sub_category_weight = pd.read_excel(Const.SUB_CATEGORY_TARGET_PATH, sheetname='category_score')
        self.kpi_weights = pd.read_excel(Const.SUB_CATEGORY_TARGET_PATH, sheetname='max_weight')
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.store_assortment = PSAssortmentDataProvider(
            self.data_provider).execute(policy_name=None)
        self.supervisor_target = self.get_supervisor_target()
        try:
            self.sub_category_assortment = pd.merge(self.store_assortment,
                                                    self.all_products.loc[:, ['product_fk', 'sub_category',
                                                                              'sub_category_fk']],
                                                    how='left', on='product_fk')
            self.sub_category_assortment = \
                self.sub_category_assortment[~self.sub_category_assortment['assortment_name'].str.contains(
                    'ASSORTMENT')]
            self.sub_category_assortment = pd.merge(self.sub_category_assortment, self.sub_category_weight, how='left',
                                                    left_on='sub_category',
                                                    right_on='Category')


        except KeyError:
            self.sub_category_assortment = pd.DataFrame()
        self.update_score_sub_category_weights()
        try:
            self.store_assortment_without_powerskus = \
                self.store_assortment[self.store_assortment['assortment_name'].str.contains('ASSORTMENT')]
        except KeyError:
            self.store_assortment_without_powerskus = pd.DataFrame()

        self.adherence_results = pd.DataFrame(columns=['product_fk', 'trax_average',
                                                       'suggested_price', 'into_interval', 'min_target', 'max_target',
                                                       'percent_range'])
        self.extra_spaces_results = pd.DataFrame(
            columns=['sub_category_fk', 'template_fk', 'count'])

        self.powersku_scores = {}
        self.powersku_empty = {}

        self.powersku_bonus = {}
        self.powersku_price = {}
        self.powersku_sos = {}

    def main_calculation(self, *args, **kwargs):
        """
        This function calculates the KPI results.
        """
        if self.scif.empty:
            return
        # these function must run first
        #  self.adherence_results = self.heinz_global_price_adherence(pd.read_excel(Const.PRICE_ADHERENCE_TEMPLATE_PATH,
        #                                                                          sheetname="Price Adherence"))
        self.adherence_results = self.heinz_global_price_adherence(self.targets)
        self.extra_spaces_results = self.heinz_global_extra_spaces()
        self.set_relevant_sub_categories()

        # this isn't relevant to the 'Perfect Score' calculation
        self.heinz_global_distribution_per_category()
        self.calculate_assortment()

        self.calculate_powersku_assortment()
        self.main_sos_calculation()
        self.calculate_powersku_price_adherence()
        self.calculate_perfect_store_extra_spaces()
        self.check_bonus_question()

        self.calculate_perfect_sub_category()

    def calculate_assortment(self):
        if self.store_assortment_without_powerskus.empty:
            return

        products_in_store = self.scif[self.scif['facings'] > 0]['product_fk'].unique().tolist()
        pass_count = 0

        total_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Distribution')
        identifier_dict = self.common_v2.get_dictionary(kpi_fk=total_kpi_fk)

        oos_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('OOS')
        oos_identifier_dict = self.common_v2.get_dictionary(kpi_fk=oos_kpi_fk)

        for row in self.store_assortment_without_powerskus.itertuples():
            result = 0
            if row.product_fk in products_in_store:
                result = 1
                pass_count += 1

            sku_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('Distribution - SKU')
            self.common_v2.write_to_db_result(sku_kpi_fk, numerator_id=row.product_fk, denominator_id=row.assortment_fk,
                                              result=result, identifier_parent=identifier_dict, should_enter=True)

            oos_result = 0 if result else 1
            oos_sku_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type('OOS - SKU')
            self.common_v2.write_to_db_result(oos_sku_kpi_fk, numerator_id=row.product_fk,
                                              denominator_id=row.assortment_fk,
                                              result=oos_result, identifier_parent=oos_identifier_dict,
                                              should_enter=True)

        number_of_products_in_assortment = len(self.store_assortment_without_powerskus)
        if number_of_products_in_assortment:
            total_result = (pass_count / float(number_of_products_in_assortment)) * 100
            oos_products = number_of_products_in_assortment - pass_count
            oos_result = (oos_products / float(number_of_products_in_assortment)) * 100
        else:
            total_result = 0
            oos_products = number_of_products_in_assortment
            oos_result = number_of_products_in_assortment
        self.common_v2.write_to_db_result(total_kpi_fk, numerator_id=Const.OWN_MANUFACTURER_FK,
                                          denominator_id=self.store_id,
                                          numerator_result=pass_count,
                                          denominator_result=number_of_products_in_assortment,
                                          result=total_result, identifier_result=identifier_dict)
        self.common_v2.write_to_db_result(oos_kpi_fk, numerator_id=Const.OWN_MANUFACTURER_FK,
                                          denominator_id=self.store_id,
                                          numerator_result=oos_products,
                                          denominator_result=number_of_products_in_assortment,
                                          result=oos_result, identifier_result=oos_identifier_dict)

    def calculate_powersku_assortment(self):
        if self.sub_category_assortment.empty:
            return 0

        sub_category_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU_SUB_CATEGORY)
        sku_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU)
        target_kpi_weight = float(
            self.kpi_weights['Score'][self.kpi_weights['KPIs'] == Const.KPI_WEIGHTS['POWERSKU']].iloc[
                0])

        kpi_weight = self.get_kpi_weight('POWERSKU')

        products_in_session = self.scif[self.scif['facings'] > 0]['product_fk'].unique().tolist()
        self.sub_category_assortment['in_session'] = \
            self.sub_category_assortment.loc[:, 'product_fk'].isin(products_in_session)

        # save PowerSKU results at SKU level
        for sku in self.sub_category_assortment[
            ['product_fk', 'sub_category_fk', 'in_session', 'sub_category']].itertuples():
            parent_dict = self.common_v2.get_dictionary(
                kpi_fk=sub_category_kpi_fk, sub_category_fk=sku.sub_category_fk)
            relevant_sub_category_df = self.sub_category_assortment[
                self.sub_category_assortment['sub_category'] == sku.sub_category]
            if relevant_sub_category_df.empty:
                sub_category_count = 0
            else:
                sub_category_count = len(relevant_sub_category_df)

            result = 1 if sku.in_session else 0

            score = result * (target_kpi_weight / float(sub_category_count))
            self.common_v2.write_to_db_result(sku_kpi_fk, numerator_id=sku.product_fk,
                                              denominator_id=sku.sub_category_fk, score=score,
                                              result=result, identifier_parent=parent_dict, should_enter=True)
        # save PowerSKU results at sub_category level

        aggregated_results = self.sub_category_assortment.groupby('sub_category_fk').agg(
            {'in_session': 'sum', 'product_fk': 'count'}).reset_index().rename(
            columns={'product_fk': 'product_count'})
        aggregated_results['percent_complete'] = \
            aggregated_results.loc[:, 'in_session'] / aggregated_results.loc[:, 'product_count']
        aggregated_results['result'] = aggregated_results['percent_complete']
        for sub_category in aggregated_results.itertuples():
            identifier_dict = self.common_v2.get_dictionary(kpi_fk=sub_category_kpi_fk,
                                                            sub_category_fk=sub_category.sub_category_fk)

            result = sub_category.result
            score = result * kpi_weight

            self.powersku_scores[sub_category.sub_category_fk] = score
            self.common_v2.write_to_db_result(sub_category_kpi_fk, numerator_id=sub_category.sub_category_fk,
                                              denominator_id=self.store_id,
                                              identifier_parent=sub_category.sub_category_fk,
                                              identifier_result=identifier_dict, result=result * 100, score=score,
                                              weight=target_kpi_weight, target=target_kpi_weight,
                                              should_enter=True)

    def heinz_global_distribution_per_category(self):
        relevant_stores = pd.DataFrame(columns=self.store_sos_policies.columns)
        for row in self.store_sos_policies.itertuples():
            policies = json.loads(row.store_policy)
            df = self.store_info
            for key, value in policies.items():
                try:
                    df_1 = df[df[key].isin(value)]
                except KeyError:
                    continue
            if not df_1.empty:
                stores = self.store_sos_policies[(self.store_sos_policies['store_policy'] == row.store_policy.encode('utf-8'))
                                                 & (
                                                         self.store_sos_policies[
                                                             'target_validity_start_date'] <= datetime.date(
                                                     self.current_date))]
                if stores.empty:
                    relevant_stores = stores
                else:
                    relevant_stores = relevant_stores.append(stores, ignore_index=True)
        relevant_stores = relevant_stores.drop_duplicates(subset=['kpi', 'sku_name', 'target', 'sos_policy'],
                                                          keep='last')
        for row in relevant_stores.itertuples():
            sos_policy = json.loads(row.sos_policy)
            numerator_key = sos_policy[self.NUMERATOR].keys()[0]
            denominator_key = sos_policy[self.DENOMINATOR].keys()[0]
            numerator_val = sos_policy[self.NUMERATOR][numerator_key]
            denominator_val = sos_policy[self.DENOMINATOR][denominator_key]
            target = row.target * 100
            if numerator_key == 'manufacturer':
                numerator_key = numerator_key + '_name'

            if denominator_key == 'sub_category' \
                    and denominator_val.lower() != 'all' \
                    and json.loads(row.store_policy).get('store_type') \
                    and len(json.loads(row.store_policy).get('store_type')) == 1:
                try:
                    denominator_id = self.all_products[self.all_products[denominator_key] == denominator_val][
                        denominator_key + '_fk'].values[0]
                    numerator_id = self.all_products[self.all_products[numerator_key] == numerator_val][
                        numerator_key.split('_')[0] + '_fk'].values[0]

                    # self.common.write_to_db_result_new_tables(fk=12, numerator_id=numerator_id,
                    #                                           numerator_result=None,
                    #                                           denominator_id=denominator_id,
                    #                                           denominator_result=None,
                    #                                           result=target)
                    self.common_v2.write_to_db_result(fk=12, numerator_id=numerator_id, numerator_result=None,
                                                      denominator_id=denominator_id, denominator_result=None,
                                                      result=target)
                except Exception as e:
                    Log.warning(denominator_key + ' - - ' + denominator_val)

    def calculate_perfect_store(self):
        pass

    def calculate_perfect_sub_category(self):
        kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.PERFECT_STORE_SUB_CATEGORY)
        parent_kpi = self.common_v2.get_kpi_fk_by_kpi_type(Const.PERFECT_STORE)

        total_score = 0
        sub_category_fk_list = []
        kpi_type_dict_scores = [self.powersku_scores, self.powersku_empty, self.powersku_price,
                                self.powersku_sos]

        for kpi_dict in kpi_type_dict_scores:
            sub_category_fk_list.extend(kpi_dict.keys())

        kpi_weight_perfect_store = 0
        if self.country in self.sub_category_weight.columns.to_list():
            kpi_weight_perfect_store = self.sub_category_weight[self.country][
                self.sub_category_weight['Category'] == Const.PERFECT_STORE_KPI_WEIGHT]

            if not kpi_weight_perfect_store.empty:
                kpi_weight_perfect_store = kpi_weight_perfect_store.iloc[0]

        unique_sub_cat_fks = list(dict.fromkeys(sub_category_fk_list))

        sub_category_fks = self.sub_category_weight.sub_category_fk.unique().tolist()
        relevant_sub_cat_list = [x for x in sub_category_fks if str(x) != 'nan']

        # relevant_sub_cat_list = self.sub_category_assortment['sub_category_fk'][
        #     self.sub_category_assortment['Category'] != pd.np.nan].unique().tolist()
        for sub_cat_fk in unique_sub_cat_fks:
            if sub_cat_fk in relevant_sub_cat_list:
                bonus_score = 0
                try:
                    bonus_score = self.powersku_bonus[sub_cat_fk]
                except:
                    pass

                sub_cat_weight = self.get_weight(sub_cat_fk)
                sub_cat_score = self.calculate_sub_category_sum(kpi_type_dict_scores, sub_cat_fk)

                result = sub_cat_score

                score = (result * sub_cat_weight) + bonus_score
                total_score += score

                self.common_v2.write_to_db_result(kpi_fk, numerator_id=sub_cat_fk,
                                                  denominator_id=self.store_id,
                                                  result=result, score=score,
                                                  identifier_parent=parent_kpi,
                                                  identifier_result=sub_cat_fk,
                                                  weight=sub_cat_weight * 100,
                                                  should_enter=True)

        self.common_v2.write_to_db_result(parent_kpi, numerator_id=Const.OWN_MANUFACTURER_FK,
                                          denominator_id=self.store_id,
                                          result=total_score, score=total_score,
                                          identifier_result=parent_kpi,
                                          target=kpi_weight_perfect_store,
                                          should_enter=True)

    def main_sos_calculation(self):
        relevant_stores = pd.DataFrame(columns=self.store_sos_policies.columns)
        for row in self.store_sos_policies.itertuples():
            policies = json.loads(row.store_policy)
            df = self.store_info
            for key, value in policies.items():
                try:
                    if key != 'additional_attribute_3':
                        df1 = df[df[key].isin(value)]
                except KeyError:
                    continue
            if not df1.empty:
                stores = \
                    self.store_sos_policies[(self.store_sos_policies['store_policy'].str.encode(
                                                'utf-8') == row.store_policy.encode('utf-8')) &
                                            (self.store_sos_policies['target_validity_start_date'] <= datetime.date(
                                                self.current_date))]
                if stores.empty:
                    relevant_stores = stores
                else:
                    relevant_stores = relevant_stores.append(stores, ignore_index=True)

        relevant_stores = relevant_stores.drop_duplicates(subset=['kpi', 'sku_name', 'target', 'sos_policy'],
                                                          keep='last')

        results_df = pd.DataFrame(columns=['sub_category', 'sub_category_fk', 'score'])

        sos_sub_category_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.SOS_SUB_CATEGORY)

        for row in relevant_stores.itertuples():
            sos_policy = json.loads(row.sos_policy)
            numerator_key = sos_policy[self.NUMERATOR].keys()[0]
            denominator_key = sos_policy[self.DENOMINATOR].keys()[0]
            numerator_val = sos_policy[self.NUMERATOR][numerator_key]
            denominator_val = sos_policy[self.DENOMINATOR][denominator_key]
            json_policy = json.loads(row.store_policy)
            kpi_fk = row.kpi

            # This is to assign the KPI to SOS_manufacturer_category_GLOBAL
            if json_policy.get('store_type') and len(json_policy.get('store_type')) > 1:
                kpi_fk = 8

            if numerator_key == 'manufacturer':
                numerator_key = numerator_key + '_name'
                # we need to include 'Philadelphia' as a manufacturer for all countries EXCEPT Chile
                if self.country == 'Chile':
                    numerator_values = [numerator_val]
                else:
                    numerator_values = [numerator_val, 'Philadelphia']
            else:
                # if the numerator isn't 'manufacturer', we just need to convert the value to a list
                numerator_values = [numerator_val]

            if denominator_key == 'sub_category':
                include_stacking_list = ['Nuts', 'DRY CHEESE', 'IWSN', 'Shredded', 'SNACK']
                if denominator_val in include_stacking_list:
                    facings_field = 'facings'
                else:
                    facings_field = 'facings_ign_stack'
            else:
                facings_field = 'facings_ign_stack'

            if denominator_key == 'sub_category' and denominator_val.lower() == 'all':
                # Here we are talkin on a KPI when the target have no denominator,
                # the calculation should be done on Numerator only
                numerator = self.scif[(self.scif[numerator_key] == numerator_val) &
                                      (self.scif['location_type'] == 'Primary Shelf')
                                      ][facings_field].sum()
                kpi_fk = 9
                denominator = None
                denominator_id = None
            else:
                numerator = self.scif[(self.scif[numerator_key].isin(numerator_values)) &
                                      (self.scif[denominator_key] == denominator_val) &
                                      (self.scif['location_type'] == 'Primary Shelf')][facings_field].sum()
                denominator = self.scif[(self.scif[denominator_key] == denominator_val) &
                                        (self.scif['location_type'] == 'Primary Shelf')][facings_field].sum()

            try:
                if denominator is not None:
                    denominator_id = self.all_products[self.all_products[denominator_key] == denominator_val][
                        denominator_key + '_fk'].values[0]
                if numerator is not None:
                    numerator_id = self.all_products[self.all_products[numerator_key] == numerator_val][
                        numerator_key.split('_')[0] + '_fk'].values[0]

                sos = 0
                if numerator and denominator:
                    sos = np.divide(float(numerator), float(denominator)) * 100
                score = 0
                target = row.target * 100
                if sos >= target:
                    score = 100

                identifier_parent = None
                should_enter = False
                if denominator_key == 'sub_category' and kpi_fk == row.kpi:
                    # if this a sub_category result, save it to the results_df for 'Perfect Store' store
                    results_df.loc[len(results_df)] = [denominator_val, denominator_id, score / 100]
                    identifier_parent = self.common_v2.get_dictionary(kpi_fk=sos_sub_category_kpi_fk,
                                                                      sub_category_fk=denominator_id)
                    should_enter = True

                manufacturer = None
                self.common_v2.write_to_db_result(kpi_fk, numerator_id=numerator_id, numerator_result=numerator,
                                                  denominator_id=denominator_id, denominator_result=denominator,
                                                  result=target, score=sos, target=target,
                                                  score_after_actions=manufacturer, identifier_parent=identifier_parent,
                                                  should_enter=should_enter)
            except Exception as e:
                Log.warning(denominator_key + ' - - ' + denominator_val)

        # if there are no sub_category sos results, there's no perfect store information to be saved
        if len(results_df) == 0:
            return 0

        # save aggregated results for each sub category
        kpi_weight = self.get_kpi_weight('SOS')
        for row in results_df.itertuples():
            identifier_result = \
                self.common_v2.get_dictionary(kpi_fk=sos_sub_category_kpi_fk,
                                              sub_category_fk=row.sub_category_fk)

            # sub_cat_weight = self.get_weight(row.sub_category_fk)
            result = row.score
            score = result * kpi_weight

            self.powersku_sos[row.sub_category_fk] = score
            # limit results so that aggregated results can only add up to 3
            self.common_v2.write_to_db_result(sos_sub_category_kpi_fk,
                                              numerator_id=row.sub_category_fk,
                                              denominator_id=self.store_id,
                                              result=row.score, score=score,
                                              identifier_parent=row.sub_category_fk,
                                              identifier_result=identifier_result,
                                              weight=kpi_weight,
                                              target=kpi_weight,
                                              should_enter=True)

    def calculate_powersku_price_adherence(self):
        adherence_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU_PRICE_ADHERENCE)
        adherence_sub_category_kpi_fk = \
            self.common_v2.get_kpi_fk_by_kpi_type(Const.POWER_SKU_PRICE_ADHERENCE_SUB_CATEGORY)

        if self.sub_category_assortment.empty:
            return False

        results = pd.merge(self.sub_category_assortment,
                           self.adherence_results, how='left', on='product_fk')
        results['into_interval'].fillna(0, inplace=True)

        for row in results.itertuples():
            parent_dict = self.common_v2.get_dictionary(kpi_fk=adherence_sub_category_kpi_fk,
                                                        sub_category_fk=row.sub_category_fk)

            score_value = 'Not Present'
            in_session = row.in_session
            if in_session:
                if not pd.isna(row.trax_average) and row.suggested_price:
                    price_in_interval = 1 if row.into_interval == 1 else 0
                    if price_in_interval == 1:
                        score_value = 'Pass'
                    else:
                        score_value = 'Fail'
                else:
                    score_value = 'No Price'

            score = Const.PRESENCE_PRICE_VALUES[score_value]
            self.common_v2.write_to_db_result(adherence_kpi_fk, numerator_id=row.product_fk,
                                              denominator_id=row.sub_category_fk, result=row.trax_average,
                                              score=score, target=row.suggested_price, numerator_result=row.min_target,
                                              denominator_result=row.max_target,
                                              weight=row.percent_range,
                                              identifier_parent=parent_dict, should_enter=True)

        aggregated_results = results.groupby('sub_category_fk').agg(
            {'into_interval': 'sum', 'product_fk': 'count'}).reset_index().rename(
            columns={'product_fk': 'product_count'})
        aggregated_results['percent_complete'] = \
            aggregated_results.loc[:, 'into_interval'] / aggregated_results.loc[:, 'product_count']

        for row in aggregated_results.itertuples():
            identifier_result = self.common_v2.get_dictionary(kpi_fk=adherence_sub_category_kpi_fk,
                                                              sub_category_fk=row.sub_category_fk)
            kpi_weight = self.get_kpi_weight('PRICE')
            result = row.percent_complete
            score = result * kpi_weight

            self.powersku_price[row.sub_category_fk] = score

            self.common_v2.write_to_db_result(adherence_sub_category_kpi_fk, numerator_id=row.sub_category_fk,
                                              denominator_id=self.store_id, result=result, score=score,
                                              numerator_result=row.into_interval, denominator_result=row.product_count,
                                              identifier_parent=row.sub_category_fk,
                                              identifier_result=identifier_result,
                                              weight=kpi_weight, target=kpi_weight,
                                              should_enter=True)

    def heinz_global_price_adherence(self, config_df):
        config_df = config_df.sort_values(by=["received_time"], ascending=False).drop_duplicates(
            subset=['start_date', 'end_date', 'ean_code', 'store_type'], keep="first")

        if config_df.empty:
            Log.warning("No external_targets data found - Price Adherence will not be calculated")
            return self.adherence_results

        self.match_product_in_scene.loc[self.match_product_in_scene['price'].isna(), 'price'] = \
            self.match_product_in_scene.loc[self.match_product_in_scene['price'].isna(), 'promotion_price']
        # =============== remove after updating logic to support promotional pricing ===============
        results_df = self.adherence_results
        my_config_df = \
            config_df[config_df['store_type'].str.encode('utf-8') == self.store_info.store_type[0].encode('utf-8')]

        products_in_session = self.scif['product_ean_code'].unique().tolist()
        products_in_session = [ean for ean in products_in_session if ean is not pd.np.nan and ean is not None]

        my_config_df = my_config_df[my_config_df['ean_code'].isin(products_in_session)]

        for row in my_config_df.itertuples():
            product_pk = \
                self.all_products[self.all_products['product_ean_code']
                                  == row.ean_code]['product_fk'].iloc[0]

            mpisc_df_price = \
                self.match_product_in_scene[(self.match_product_in_scene['product_fk'] == product_pk) |
                                            (self.match_product_in_scene[
                                                 'substitution_product_fk'] == product_pk)]['price']
            try:
                suggested_price = float(row.suggested_price)
            except Exception as e:
                Log.error("Product with ean_code {} is not in the configuration file for customer type {}"
                          .format(row.ean_code, self.store_info.store_type[0].encode('utf-8')))
                break
            percentage_weight = int(row.percentage_weight)
            upper_percentage = (100 + percentage_weight) / float(100)
            lower_percentage = (100 - percentage_weight) / float(100)
            min_price = suggested_price * lower_percentage
            max_price = suggested_price * upper_percentage
            percentage_sku = percentage_weight
            into_interval = 0
            prices_sum = 0
            count = 0
            trax_average = None
            for price in mpisc_df_price:
                if price and pd.notna(price):
                    prices_sum += price
                    count += 1

            if prices_sum > 0:
                trax_average = prices_sum / count
                into_interval = 0

            if not np.isnan(suggested_price):
                if min_price <= trax_average <= max_price:
                    into_interval = 100

            results_df.loc[len(results_df)] = [product_pk, trax_average,
                                               suggested_price, into_interval / 100, min_price, max_price,
                                               percentage_sku]

            self.common_v2.write_to_db_result(10, numerator_id=product_pk,
                                              numerator_result=suggested_price,
                                              denominator_id=product_pk,
                                              denominator_result=trax_average,
                                              result=row.percentage_weight,
                                              score=into_interval)
            if trax_average:
                mark_up = (np.divide(np.divide(float(trax_average), float(1.13)),
                                     float(suggested_price)) - 1) * 100

                self.common_v2.write_to_db_result(11, numerator_id=product_pk,
                                                  numerator_result=suggested_price,
                                                  denominator_id=product_pk,
                                                  denominator_result=trax_average,
                                                  score=mark_up,
                                                  result=mark_up)

        return results_df

    def calculate_perfect_store_extra_spaces(self):
        extra_spaces_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(
            Const.PERFECT_STORE_EXTRA_SPACES_SUB_CATEGORY)

        sub_cats_for_store = self.relevant_sub_categories

        if self.extra_spaces_results.empty:
            pass

        try:
            relevant_sub_categories = [x.strip() for x in self.extra_spaces_template[
                self.extra_spaces_template['country'].str.encode('utf-8') == self.country.encode('utf-8')][
                'sub_category'].iloc[0].split(',')]
        except IndexError:
            Log.warning(
                'No relevant sub_categories for the Extra Spaces KPI found for the following country: {}'.format(
                    self.country))

        self.extra_spaces_results = pd.merge(self.extra_spaces_results,
                                             self.all_products.loc[:, [
                                                                          'sub_category_fk',
                                                                          'sub_category']].dropna().drop_duplicates(),
                                             how='left', on='sub_category_fk')

        relevant_extra_spaces = \
            self.extra_spaces_results[self.extra_spaces_results['sub_category'].isin(
                relevant_sub_categories)]
        kpi_weight = self.get_kpi_weight('EXTRA')
        for row in relevant_extra_spaces.itertuples():
            self.powersku_empty[row.sub_category_fk] = 1 * kpi_weight
            score = result = 1

            if row.sub_category_fk in sub_cats_for_store:
                sub_cats_for_store.remove(row.sub_category_fk)

            self.common_v2.write_to_db_result(extra_spaces_kpi_fk, numerator_id=row.sub_category_fk,
                                              denominator_id=row.template_fk, result=result, score=score,
                                              identifier_parent=row.sub_category_fk,
                                              target=1, should_enter=True)

        for sub_cat_fk in sub_cats_for_store:
            result = score = 0
            self.powersku_empty[sub_cat_fk] = 0
            self.common_v2.write_to_db_result(extra_spaces_kpi_fk, numerator_id=sub_cat_fk,
                                              denominator_id=0, result=result, score=score,
                                              identifier_parent=sub_cat_fk,
                                              target=1, should_enter=True)

    def heinz_global_extra_spaces(self):
        try:
            supervisor = self.store_info['additional_attribute_3'][0]
            store_target = -1
            # for row in self.store_sos_policies.itertuples():
            #     policies = json.loads(row.store_policy)
            #     for key, value in policies.items():
            #         try:
            #             if key == 'additional_attribute_3' and value[0] == supervisor:
            #                 store_target = row.target
            #                 break
            #         except KeyError:
            #             continue

            for row in self.supervisor_target.itertuples():
                try:
                    if row.supervisor == supervisor:
                        store_target = row.target
                        break
                except:
                    continue
        except Exception as e:
            Log.error("Supervisor target is not configured for the extra spaces report ")
            raise e

        results_df = self.extra_spaces_results

        # limit to only secondary scenes
        relevant_scif = self.scif[(self.scif['location_type_fk'] == float(2)) &
                                  (self.scif['facings'] > 0)]
        if relevant_scif.empty:
            return results_df
        # aggregate facings for every scene/sub_category combination in the visit
        relevant_scif = \
            relevant_scif.groupby(['scene_fk', 'template_fk', 'sub_category_fk'], as_index=False)['facings'].sum()
        # sort sub_categories by number of facings, largest first
        relevant_scif = relevant_scif.sort_values(['facings'], ascending=False)
        # drop all but the sub_category with the largest number of facings for each scene
        relevant_scif = relevant_scif.drop_duplicates(subset=['scene_fk'], keep='first')

        for row in relevant_scif.itertuples():
            results_df.loc[len(results_df)] = [row.sub_category_fk, row.template_fk, row.facings]
            self.common_v2.write_to_db_result(13, numerator_id=row.template_fk,
                                              numerator_result=row.facings,
                                              denominator_id=row.sub_category_fk,
                                              denominator_result=row.facings,
                                              context_id=row.scene_fk,
                                              result=store_target)

        return results_df

    def check_bonus_question(self):
        bonus_kpi_fk = self.common_v2.get_kpi_fk_by_kpi_type(Const.BONUS_QUESTION_SUB_CATEGORY)
        bonus_weight = self.kpi_weights['Score'][self.kpi_weights['KPIs'] == Const.KPI_WEIGHTS['Bonus']].iloc[0]

        sub_category_fks = self.sub_category_weight.sub_category_fk.unique().tolist()
        sub_category_fks = [x for x in sub_category_fks if str(x) != 'nan']
        if self.survey.check_survey_answer(('question_fk', Const.BONUS_QUESTION_FK), 'Yes,yes,si,Si'):
            result = 1
        else:
            result = 0

        for sub_cat_fk in sub_category_fks:
            sub_cat_weight = self.get_weight(sub_cat_fk)

            score = result * sub_cat_weight
            target_weight = bonus_weight * sub_cat_weight
            self.powersku_bonus[sub_cat_fk] = score

            self.common_v2.write_to_db_result(bonus_kpi_fk, numerator_id=sub_cat_fk,
                                              denominator_id=self.store_id,
                                              result=result, score=score, identifier_parent=sub_cat_fk,
                                              weight=target_weight, target=target_weight,
                                              should_enter=True)

    def commit_results_data(self):
        self.common_v2.commit_results_data()

    def update_score_sub_category_weights(self):
        all_sub_category_fks = self.all_products[['sub_category', 'sub_category_fk']].drop_duplicates()
        self.sub_category_weight = pd.merge(self.sub_category_weight, all_sub_category_fks, left_on='Category',
                                            right_on='sub_category',
                                            how='left')

    def get_weight(self, sub_category_fk):
        weight_value = 0

        if self.country in self.sub_category_weight.columns.to_list():
            weight_df = self.sub_category_weight[self.country][
                (self.sub_category_weight.sub_category_fk == sub_category_fk)]
            if weight_df.empty:
                return 0

            weight_value = weight_df.iloc[0]

            if pd.isna(weight_value):
                weight_value = 0

        weight = weight_value * 0.01
        return weight

    def get_kpi_weight(self, kpi_name):
        weight = self.kpi_weights['Score'][self.kpi_weights['KPIs'] == Const.KPI_WEIGHTS[kpi_name]].iloc[0]
        return weight

    def get_supervisor_target(self):
        supervisor_target = self.targets[self.targets['kpi_type'] == 'Extra Spaces']
        return supervisor_target

    def calculate_sub_category_sum(self, dict_list, sub_cat_fk):
        total_score = 0
        for item in dict_list:
            try:
                total_score += item[sub_cat_fk]
            except:
                pass

        return total_score

    def set_relevant_sub_categories(self):
        if self.country in self.sub_category_weight.columns.to_list():

            df = self.sub_category_weight[['Category', 'sub_category_fk', self.country]].dropna()
            self.relevant_sub_categories = df.sub_category_fk.to_list()
        else:
            self.relevant_sub_categories = []
示例#16
0
class GSKRUToolBox:
    def __init__(self, data_provider, output):

        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.own_manufacturer_id = int(self.data_provider.own_manufacturer[
            self.data_provider.own_manufacturer['param_name'] ==
            'manufacturer_id']['param_value'].iloc[0])

        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)

        self.common = Common(self.data_provider)
        self.kpi_static_data = self.common.get_kpi_static_data()

        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.store_type = self.ps_data_provider.session_info.store_type
        self.store_channel = self.ps_data_provider.session_info.additional_attribute_11.encode(
            'utf-8')
        self.store_format = self.ps_data_provider.session_info.additional_attribute_12.encode(
            'utf-8')
        self.retailer_fk = self.ps_data_provider.session_info.retailer_fk

        self.set_up_template = None
        self.gsk_generator = None
        self.core_range_targets = {}

        self.set_up_data = LocalConsts.SET_UP_DATA
        self.set_up_template = pd.read_excel(
            os.path.join(os.path.dirname(os.path.realpath(__file__)), '..',
                         'Data', 'gsk_set_up.xlsx'),
            sheet_name='Functional KPIs All Store',
            keep_default_na=False)
        self.gsk_generator = GSKGenerator(self.data_provider, self.output,
                                          self.common, self.set_up_template)

    def main_calculation(self, *args, **kwargs):
        """
        This function calculates the KPI results.
        """

        # Global KPIs

        # All Store KPIs
        assortment_store_dict = self.gsk_generator.availability_store_function(
            custom_suffix='_Stacking_Included')
        self.common.save_json_to_new_tables(assortment_store_dict)

        assortment_category_dict = self.gsk_generator.availability_category_function(
            custom_suffix='_Stacking_Included')
        self.common.save_json_to_new_tables(assortment_category_dict)

        assortment_subcategory_dict = self.gsk_generator.availability_subcategory_function(
            custom_suffix='_Stacking_Included')
        self.common.save_json_to_new_tables(assortment_subcategory_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_whole_store_function(
            custom_suffix='_Stacking_Included',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_by_category_function(
            custom_suffix='_Stacking_Included',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_by_sub_category_function(
            custom_suffix='_Stacking_Included',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_whole_store_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_category_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_sub_category_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        # Main Shelf KPIs
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.set_up_template = pd.read_excel(
            os.path.join(os.path.dirname(os.path.realpath(__file__)), '..',
                         'Data', 'gsk_set_up.xlsx'),
            sheet_name='Functional KPIs Main Shelf',
            keep_default_na=False)
        self.gsk_generator.set_up_file = self.set_up_template
        self.gsk_generator.tool_box.set_up_file = self.gsk_generator.set_up_file
        self.gsk_generator.tool_box.set_up_data = LocalConsts.SET_UP_DATA.copy(
        )
        # self.gsk_generator = GSKGenerator(self.data_provider, self.output, self.common, self.set_up_template)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_whole_store_function(
            custom_suffix='_Stacking_Included_Main_Shelf',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_by_category_function(
            custom_suffix='_Stacking_Included_Main_Shelf',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_by_sub_category_function(
            custom_suffix='_Stacking_Included_Main_Shelf',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_whole_store_function(
            custom_suffix='_Main_Shelf')
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_category_function(
            custom_suffix='_Main_Shelf')
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_sub_category_function(
            custom_suffix='_Main_Shelf')
        self.common.save_json_to_new_tables(linear_sos_dict)

        # Secondary Shelf KPIs
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.set_up_template = pd.read_excel(
            os.path.join(os.path.dirname(os.path.realpath(__file__)), '..',
                         'Data', 'gsk_set_up.xlsx'),
            sheet_name='Functional KPIs Secondary Shelf',
            keep_default_na=False)
        self.gsk_generator.set_up_file = self.set_up_template
        self.gsk_generator.tool_box.set_up_file = self.gsk_generator.set_up_file
        self.gsk_generator.tool_box.set_up_data = LocalConsts.SET_UP_DATA.copy(
        )
        # self.gsk_generator = GSKGenerator(self.data_provider, self.output, self.common, self.set_up_template)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_whole_store_function(
            custom_suffix='_Stacking_Included_Secondary_Shelf',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_by_category_function(
            custom_suffix='_Stacking_Included_Secondary_Shelf',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        facings_sos_dict = self.gsk_generator.gsk_global_facings_by_sub_category_function(
            custom_suffix='_Stacking_Included_Secondary_Shelf',
            fractional_facings_parameters=LocalConsts.
            FRACTIONAL_FACINGS_PARAMETERS)
        self.common.save_json_to_new_tables(facings_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_whole_store_function(
            custom_suffix='_Secondary_Shelf')
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_category_function(
            custom_suffix='_Secondary_Shelf')
        self.common.save_json_to_new_tables(linear_sos_dict)

        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_by_sub_category_function(
            custom_suffix='_Secondary_Shelf')
        self.common.save_json_to_new_tables(linear_sos_dict)

        # Local KPIs
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.set_up_template = pd.read_excel(
            os.path.join(os.path.dirname(os.path.realpath(__file__)), '..',
                         'Data', 'gsk_set_up.xlsx'),
            sheet_name='Functional KPIs Local',
            keep_default_na=False)
        self.gsk_generator.set_up_file = self.set_up_template
        self.gsk_generator.tool_box.set_up_file = self.gsk_generator.set_up_file
        self.gsk_generator.tool_box.set_up_data = LocalConsts.SET_UP_DATA.copy(
        )
        # self.gsk_generator = GSKGenerator(self.data_provider, self.output, self.common, self.set_up_template)

        # SOA
        soa_dict = self.gsk_soa_function()
        self.common.save_json_to_new_tables(soa_dict)

        # # Core Range Assortment - disabled until phase 2
        # cra_dict = self.gsk_cra_function()
        # self.common.save_json_to_new_tables(cra_dict)

        self.common.commit_results_data()

        return

    def gsk_soa_function(self):

        results = []

        kpi_soa_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.SOA_KPI)
        kpi_soa_manufacturer_internal_target_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.SOA_MANUFACTURER_INTERNAL_TARGET_KPI)
        kpi_soa_manufacturer_external_target_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.SOA_MANUFACTURER_EXTERNAL_TARGET_KPI)
        kpi_soa_subcat_internal_target_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.SOA_SUBCAT_INTERNAL_TARGET_KPI)
        kpi_soa_subcat_external_target_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.SOA_SUBCAT_EXTERNAL_TARGET_KPI)

        identifier_internal = self.common.get_dictionary(
            manufacturer_fk=self.own_manufacturer_id,
            kpi_fk=kpi_soa_manufacturer_internal_target_fk)
        identifier_external = self.common.get_dictionary(
            manufacturer_fk=self.own_manufacturer_id,
            kpi_fk=kpi_soa_manufacturer_external_target_fk)

        targets = \
            self.ps_data_provider.get_kpi_external_targets(kpi_fks=[kpi_soa_fk],
                                                           key_filters={'additional_attribute_11': self.store_channel,
                                                                        'additional_attribute_12': self.store_format})

        # if targets.empty:
        #     Log.warning('No SOA targets defined for this session')
        # else:

        self.gsk_generator.tool_box. \
            extract_data_set_up_file(LocalConsts.SOA, self.set_up_data, LocalConsts.KPI_DICT)
        df = self.gsk_generator.tool_box.tests_by_template(
            LocalConsts.SOA, self.scif, self.set_up_data)
        df, facings_column = self.df_filter_by_stacking(df, LocalConsts.SOA)

        # Sub-Category
        for sub_category_fk in df[
                ScifConsts.SUB_CATEGORY_FK].unique().tolist():

            numerator_result = len(
                df[(df[ScifConsts.MANUFACTURER_FK] == self.own_manufacturer_id)
                   & (df[ScifConsts.SUB_CATEGORY_FK] == sub_category_fk)][
                       ScifConsts.PRODUCT_FK].unique().tolist())
            denominator_result = len(
                df[df[ScifConsts.SUB_CATEGORY_FK] == sub_category_fk][
                    ScifConsts.PRODUCT_FK].unique().tolist())
            result = round(float(numerator_result) / float(denominator_result), 4) \
                if numerator_result != 0 and denominator_result != 0 \
                else 0

            target = targets[targets['sub_category_fk'] ==
                             sub_category_fk]['internal_target'].values
            target = float(target[0]) if len(target) > 0 else None
            target = target / 100 if target else None
            if target:
                # score = 1 if result >= target else 0
                score = round(result / target, 4)
            else:
                score = 0
            results.append({
                'fk': kpi_soa_subcat_internal_target_fk,
                SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer_id,
                SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
                SessionResultsConsts.DENOMINATOR_ID: self.store_id,
                SessionResultsConsts.DENOMINATOR_RESULT: denominator_result,
                SessionResultsConsts.CONTEXT_ID: sub_category_fk,
                SessionResultsConsts.RESULT: result,
                SessionResultsConsts.TARGET: target,
                SessionResultsConsts.SCORE: score,
                'identifier_parent': identifier_internal,
                'should_enter': True
            })

            self.core_range_targets.update({sub_category_fk: target})

            target = targets[targets['sub_category_fk'] ==
                             sub_category_fk]['external_target'].values
            target = float(target[0]) if len(target) > 0 else None
            target = target / 100 if target else None
            if target:
                # score = 1 if result >= target else 0
                score = round(result / target, 4)
            else:
                score = 0
            results.append({
                'fk': kpi_soa_subcat_external_target_fk,
                SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer_id,
                SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
                SessionResultsConsts.DENOMINATOR_ID: self.store_id,
                SessionResultsConsts.DENOMINATOR_RESULT: denominator_result,
                SessionResultsConsts.CONTEXT_ID: sub_category_fk,
                SessionResultsConsts.RESULT: result,
                SessionResultsConsts.TARGET: target,
                SessionResultsConsts.SCORE: score,
                'identifier_parent': identifier_external,
                'should_enter': True
            })

        # Manufacturer
        numerator_result = len(
            df[df[ScifConsts.MANUFACTURER_FK] == self.own_manufacturer_id][
                ScifConsts.PRODUCT_FK].unique().tolist())
        denominator_result = len(df[ScifConsts.PRODUCT_FK].unique().tolist())
        result = round(float(numerator_result) / float(denominator_result), 4) \
            if numerator_result != 0 and denominator_result != 0 \
            else 0

        target = targets[
            targets['sub_category_fk'].isnull()]['internal_target'].values
        target = float(target[0]) if len(target) > 0 else None
        target = target / 100 if target else None
        if target:
            # score = 1 if result >= target else 0
            score = round(result / target, 4)
        else:
            score = 0
        results.append({
            'fk': kpi_soa_manufacturer_internal_target_fk,
            SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer_id,
            SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
            SessionResultsConsts.DENOMINATOR_ID: self.store_id,
            SessionResultsConsts.DENOMINATOR_RESULT: denominator_result,
            SessionResultsConsts.RESULT: result,
            SessionResultsConsts.TARGET: target,
            SessionResultsConsts.SCORE: score,
            'identifier_result': identifier_internal,
            'should_enter': True
        })

        target = targets[
            targets['sub_category_fk'].isnull()]['external_target'].values
        target = float(target[0]) if len(target) > 0 else None
        target = target / 100 if target else None
        if target:
            # score = 1 if result >= target else 0
            score = round(result / target, 4)
        else:
            score = 0
        results.append({
            'fk': kpi_soa_manufacturer_external_target_fk,
            SessionResultsConsts.NUMERATOR_ID: self.own_manufacturer_id,
            SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
            SessionResultsConsts.DENOMINATOR_ID: self.store_id,
            SessionResultsConsts.DENOMINATOR_RESULT: denominator_result,
            SessionResultsConsts.RESULT: result,
            SessionResultsConsts.TARGET: target,
            SessionResultsConsts.SCORE: score,
            'identifier_result': identifier_external,
            'should_enter': True
        })

        return results

    def gsk_cra_function(self):

        results = []

        kpi_cra_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.CRA_KPI)
        kpi_cra_manufacturer_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.CRA_MANUFACTURER_KPI)
        kpi_cra_subcat_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.CRA_SUBCAT_KPI)
        kpi_cra_subcat_by_product_fk = \
            self.common.get_kpi_fk_by_kpi_type(LocalConsts.CRA_SUBCAT_BY_PRODUCT_KPI)

        identifier_manufacturer = self.common.get_dictionary(
            manufacturer_fk=self.own_manufacturer_id,
            kpi_fk=kpi_cra_manufacturer_fk)

        total_cra_size_target = 0
        total_cra_size_actual = 0

        targets = \
            self.ps_data_provider.get_kpi_external_targets(kpi_fks=[kpi_cra_fk],
                                                           key_filters={'additional_attribute_12': self.store_format,
                                                                        'retailer_fk': self.retailer_fk})

        if targets.empty:
            Log.warning('No CRA targets defined for this session')
        else:

            self.gsk_generator.tool_box. \
                extract_data_set_up_file(LocalConsts.CRA, self.set_up_data, LocalConsts.KPI_DICT)
            df = self.gsk_generator.tool_box.tests_by_template(
                LocalConsts.CRA, self.scif, self.set_up_data)
            df, facings_column = self.df_filter_by_stacking(
                df, LocalConsts.CRA)

            df = df[df[ScifConsts.SUB_CATEGORY_FK].notnull()][
                [ScifConsts.SUB_CATEGORY_FK, ScifConsts.PRODUCT_FK, facings_column]]\
                .groupby([ScifConsts.SUB_CATEGORY_FK, ScifConsts.PRODUCT_FK]).agg({facings_column: 'sum'})\
                .reset_index()
            df = df.merge(
                targets[['sub_category_fk', 'product_fk', 'priority']],
                how='left',
                left_on=[ScifConsts.SUB_CATEGORY_FK, ScifConsts.PRODUCT_FK],
                right_on=['sub_category_fk', 'product_fk'])
            df['unique_product_id'] = \
                df.apply(lambda r:
                         'P' + str(r['priority']) if pd.notnull(r['priority']) else 'N' + str(r['product_fk']), axis=1)

            # Sub-Category
            target_subcat_fks = set(
                targets['sub_category_fk'].unique().tolist()) & set(
                    self.core_range_targets.keys())
            for sub_category_fk in target_subcat_fks:

                identifier_subcat = self.common.get_dictionary(
                    manufacturer_fk=self.own_manufacturer_id,
                    sub_category_fk=sub_category_fk,
                    kpi_fk=kpi_cra_subcat_fk)

                if sub_category_fk not in self.core_range_targets.keys():
                    numerator_result = denominator_result = result = score = 0
                else:
                    subcat_size = len(
                        df[df[ScifConsts.SUB_CATEGORY_FK] == sub_category_fk]
                        ['unique_product_id'].unique().tolist())
                    core_range_target = self.core_range_targets[
                        sub_category_fk]
                    cra_priority = round(
                        subcat_size *
                        core_range_target if core_range_target else 0)

                    cra_products_target = targets[
                        (targets['sub_category_fk'] == sub_category_fk)
                        & (targets['priority'] <= cra_priority)][[
                            'product_fk', 'priority'
                        ]]
                    cra_products_actual = df[
                        (df[ScifConsts.SUB_CATEGORY_FK] == sub_category_fk)
                        & (df['priority'] <= cra_priority)][[
                            ScifConsts.PRODUCT_FK, facings_column
                        ]]

                    cra_size_target = len(
                        targets[(targets['sub_category_fk'] == sub_category_fk)
                                & (targets['priority'] <= cra_priority)]
                        ['priority'].unique().tolist())
                    cra_size_actual = len(
                        df[(df[ScifConsts.SUB_CATEGORY_FK] == sub_category_fk)
                           & (df['priority'] <= cra_priority)]
                        ['priority'].unique().tolist())

                    if cra_size_target == 0:
                        numerator_result = denominator_result = result = score = 0
                    else:

                        # Product
                        for i, product in cra_products_target.iterrows():

                            numerator_result = \
                                cra_products_actual[cra_products_actual[ScifConsts.PRODUCT_FK] ==
                                                    product['product_fk']][facings_column].sum()
                            denominator_result = product['priority']
                            result = 1 if numerator_result else 0
                            score = result

                            results.append({
                                'fk':
                                kpi_cra_subcat_by_product_fk,
                                SessionResultsConsts.NUMERATOR_ID:
                                product['product_fk'],
                                SessionResultsConsts.NUMERATOR_RESULT:
                                numerator_result,
                                SessionResultsConsts.DENOMINATOR_ID:
                                self.own_manufacturer_id,
                                SessionResultsConsts.DENOMINATOR_RESULT:
                                denominator_result,
                                SessionResultsConsts.CONTEXT_ID:
                                sub_category_fk,
                                SessionResultsConsts.RESULT:
                                result,
                                SessionResultsConsts.SCORE:
                                score,
                                'identifier_parent':
                                identifier_subcat,
                                'should_enter':
                                True
                            })

                        numerator_result = cra_size_actual
                        denominator_result = cra_size_target
                        result = round(float(numerator_result) / float(denominator_result), 4) \
                            if numerator_result != 0 and denominator_result != 0 \
                            else 0
                        score = result

                        total_cra_size_target += cra_size_target
                        total_cra_size_actual += cra_size_actual

                results.append({
                    'fk': kpi_cra_subcat_fk,
                    SessionResultsConsts.NUMERATOR_ID:
                    self.own_manufacturer_id,
                    SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
                    SessionResultsConsts.DENOMINATOR_ID: self.store_id,
                    SessionResultsConsts.DENOMINATOR_RESULT:
                    denominator_result,
                    SessionResultsConsts.CONTEXT_ID: sub_category_fk,
                    SessionResultsConsts.RESULT: result,
                    SessionResultsConsts.SCORE: score,
                    'identifier_parent': identifier_manufacturer,
                    'identifier_result': identifier_subcat,
                    'should_enter': True
                })

            # Manufacturer
            if target_subcat_fks:
                numerator_result = total_cra_size_actual
                denominator_result = total_cra_size_target
                result = round(float(total_cra_size_actual) / float(total_cra_size_target), 4) \
                    if numerator_result != 0 and denominator_result != 0 \
                    else 0
                score = result

                results.append({
                    'fk': kpi_cra_manufacturer_fk,
                    SessionResultsConsts.NUMERATOR_ID:
                    self.own_manufacturer_id,
                    SessionResultsConsts.NUMERATOR_RESULT: numerator_result,
                    SessionResultsConsts.DENOMINATOR_ID: self.store_id,
                    SessionResultsConsts.DENOMINATOR_RESULT:
                    denominator_result,
                    SessionResultsConsts.RESULT: result,
                    SessionResultsConsts.SCORE: score,
                    'identifier_result': identifier_manufacturer,
                    'should_enter': True
                })

        return results

    def df_filter_by_stacking(self, df, kpi_type):
        include_stacking = self.set_up_data.get(
            (GlobalConsts.INCLUDE_STACKING, kpi_type), True)
        facings_column = ScifConsts.FACINGS
        if not include_stacking:
            facings_column = ScifConsts.FACINGS_IGN_STACK
        df = df[df[facings_column] > 0]
        return df, facings_column
示例#17
0
class ToolBox(GlobalSessionToolBox):
    def __init__(self, data_provider, output):
        GlobalSessionToolBox.__init__(self, data_provider, output)
        self.own_manufacturer_fk = int(self.data_provider.own_manufacturer.param_value.values[0])
        self.parser = Parser
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.assortment = Assortment(self.data_provider, self.output)
        self.ps_data = PsDataProvider(self.data_provider, self.output)
        self.kpi_external_targets = self.ps_data.get_kpi_external_targets(key_fields=Consts.KEY_FIELDS,
                                                                          data_fields=Consts.DATA_FIELDS)
    def main_calculation(self):
        self.calculate_score_sos()
        self.calculate_oos_and_distribution(assortment_type="Core")
        self.calculate_oos_and_distribution(assortment_type="Launch")
        self.calculate_oos_and_distribution(assortment_type="Focus")
        self.calculate_hierarchy_sos(calculation_type='FACINGS')
        self.calculate_hierarchy_sos(calculation_type='LINEAR')

    @kpi_runtime()
    def calculate_oos_and_distribution(self, assortment_type):
        dis_numerator = total_facings = 0
        oos_store_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=assortment_type + Consts.OOS)
        oos_sku_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=assortment_type + Consts.OOS_SKU)
        dis_store_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=assortment_type + Consts.DISTRIBUTION)
        dis_cat_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=assortment_type + Consts.DISTRIBUTION_CAT)
        dis_sku_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=assortment_type + Consts.DISTRIBUTION_SKU)
        assortment_df = self.assortment.get_lvl3_relevant_ass()
        assortment_df = assortment_df[assortment_df['kpi_fk_lvl3'] == dis_sku_kpi_fk]
        product_fks = assortment_df['product_fk'].tolist()
        categories = list(set(self.all_products[self.all_products['product_fk'].isin(product_fks)]['category_fk']))
        categories_dict = dict.fromkeys(categories, (0, 0))

        # sku level distribution
        for sku in product_fks:
            # 2 for distributed and 1 for oos
            category_fk = self.all_products[self.all_products['product_fk'] == sku]['category_fk'].values[0]
            product_df = self.scif[self.scif['product_fk'] == sku]
            if product_df.empty:
                categories_dict[category_fk] = map(sum, zip(categories_dict[category_fk], [0, 1]))
                result = 1
                facings = 0
                # Saving OOS only if product wasn't in store
                self.common.write_to_db_result(fk=oos_sku_kpi_fk, numerator_id=sku, denominator_id=category_fk,
                                               result=result, numerator_result=result, denominator_result=result,
                                               score=facings, identifier_parent=assortment_type + "_OOS",
                                               should_enter=True)
            else:
                categories_dict[category_fk] = map(sum, zip(categories_dict[category_fk], [1, 1]))
                result = 2
                facings = product_df['facings'].values[0]
                dis_numerator += 1
                total_facings += facings
            self.common.write_to_db_result(fk=dis_sku_kpi_fk, numerator_id=sku, denominator_id=category_fk,
                                           result=result, numerator_result=result, denominator_result=result,
                                           score=facings, should_enter=True,
                                           identifier_parent=assortment_type + "_DIS_CAT_{}".format(str(category_fk)))

        # category level distribution
        for category_fk in categories_dict.keys():
            cat_numerator, cat_denominator = categories_dict[category_fk]
            cat_result = self.get_result(cat_numerator, cat_denominator)
            self.common.write_to_db_result(fk=dis_cat_kpi_fk, numerator_id=category_fk,
                                           denominator_id=self.store_id, result=cat_result, should_enter=True,
                                           numerator_result=cat_numerator, denominator_result=cat_denominator,
                                           score=cat_result, identifier_parent=assortment_type + "_DIS",
                                           identifier_result=assortment_type + "_DIS_CAT_{}".format(str(category_fk)))

        # store level oos and distribution
        denominator = len(product_fks)
        dis_result = self.get_result(dis_numerator, denominator)
        oos_result = 1 - dis_result
        oos_numerator = denominator - dis_numerator
        self.common.write_to_db_result(fk=oos_store_kpi_fk, numerator_id=self.own_manufacturer_fk,
                                       denominator_id=self.store_id, result=oos_result, numerator_result=oos_numerator,
                                       denominator_result=denominator, score=total_facings,
                                       identifier_result=assortment_type + "_OOS")
        self.common.write_to_db_result(fk=dis_store_kpi_fk, numerator_id=self.own_manufacturer_fk,
                                       denominator_id=self.store_id, result=dis_result, numerator_result=dis_numerator,
                                       denominator_result=denominator, score=total_facings,
                                       identifier_result=assortment_type + "_DIS")

    def get_kpi_fks(self, kpis_list):
        for kpi in kpis_list:
            self.common.get_kpi_fk_by_kpi_type(kpi_type=kpi)

    @staticmethod
    def calculate_sos_res(numerator, denominator):
        if denominator == 0:
            return 0, 0, 0
        result = round(numerator / float(denominator), 3)
        return result, numerator, denominator

    @kpi_runtime()
    def calculate_score_sos(self):
        relevant_template = self.kpi_external_targets[self.kpi_external_targets[ExternalTargetsConsts.OPERATION_TYPE]
                                                      == Consts.SOS_KPIS]
        relevant_rows = relevant_template.copy()
        lsos_score_kpi_fk = self.common.get_kpi_fk_by_kpi_type(Consts.LSOS_SCORE_KPI)
        store_denominator = len(relevant_rows)
        store_numerator = 0
        for i, kpi_row in relevant_template.iterrows():
            kpi_fk, num_type, num_value, deno_type, deno_value, target, target_range = kpi_row[Consts.RELEVANT_FIELDS]
            numerator_filters, denominator_filters = self.get_num_and_den_filters(num_type, num_value, deno_type,
                                                                                  deno_value)
            # Only straussil SKUs
            numerator_filters['manufacturer_fk'] = self.own_manufacturer_fk
            denominator_filters['manufacturer_fk'] = self.own_manufacturer_fk
            numerator_df = self.parser.filter_df(conditions=numerator_filters, data_frame_to_filter=self.scif)
            denominator_df = self.parser.filter_df(conditions=denominator_filters, data_frame_to_filter=self.scif)
            numerator_result = numerator_df['gross_len_ign_stack'].sum()
            denominator_result = denominator_df['gross_len_ign_stack'].sum()
            lsos_result = self.get_result(numerator_result, denominator_result)
            score = 1 if ((target - target_range) <= lsos_result <= (target + target_range)) else 0
            store_numerator += score
            self.common.write_to_db_result(fk=kpi_fk, numerator_id=self.own_manufacturer_fk,
                                           denominator_id=self.store_id, should_enter=True, target=target,
                                           numerator_result=numerator_result, denominator_result=denominator_result,
                                           result=lsos_result, score=score, identifier_parent='LSOS_SCORE',
                                           weight=target_range)
        store_result = self.get_result(store_numerator, store_denominator)
        self.common.write_to_db_result(fk=lsos_score_kpi_fk, numerator_id=self.own_manufacturer_fk,
                                       denominator_id=self.store_id, should_enter=True, target=store_denominator,
                                       numerator_result=store_numerator, denominator_result=store_denominator,
                                       result=store_numerator, score=store_result, identifier_result='LSOS_SCORE')

    @staticmethod
    def get_num_and_den_filters(numerator_type, numerator_value, denominator_type, denominator_value):
        if type(numerator_value) != list:
            numerator_value = [numerator_value]
        if type(denominator_value) != list:
            denominator_value = [denominator_value]
        numerator_filters = {numerator_type: numerator_value}
        denominator_filters = {denominator_type: denominator_value}
        return numerator_filters, denominator_filters

    @staticmethod
    def get_result(numerator, denominator):
        if denominator == 0:
            return 0
        else:
            return round(numerator / float(denominator), 4)

    def calculate_hierarchy_sos(self, calculation_type):
        brand_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=(calculation_type + Consts.SOS_BY_BRAND))
        brand_category_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=(calculation_type +
                                                                             Consts.SOS_BY_CAT_BRAND))
        sku_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=(calculation_type + Consts.SOS_BY_CAT_BRAND_SKU))
        calculation_param = "facings_ign_stack" if calculation_type == 'FACINGS' else "gross_len_ign_stack"
        sos_df = self.parser.filter_df(conditions={'rlv_sos_sc': 1, 'product_type': ['SKU', 'Empty']},
                                       data_frame_to_filter=self.scif)
        # brand level sos
        session_brands = set(sos_df['brand_fk'])
        brand_den = sos_df[calculation_param].sum()
        for brand_fk in session_brands:
            filters = {'brand_fk': brand_fk}
            brand_df = self.parser.filter_df(conditions=filters, data_frame_to_filter=sos_df)
            if brand_df.empty:
                continue
            manufacturer_fk = brand_df['manufacturer_fk'].values[0]
            brand_num = brand_df[calculation_param].sum()
            if brand_num == 0:
                continue
            brand_res, brand_num, brand_den = self.calculate_sos_res(brand_num, brand_den)
            self.common.write_to_db_result(fk=brand_kpi_fk, numerator_id=brand_fk,
                                           denominator_id=manufacturer_fk,
                                           result=brand_res, numerator_result=brand_num, denominator_result=brand_den,
                                           score=brand_res,
                                           identifier_result="{}_SOS_brand_{}".format(calculation_type, str(brand_fk)))
            # brand-category level sos
            brand_categories = set(self.parser.filter_df(conditions=filters,
                                                         data_frame_to_filter=sos_df)['category_fk'])
            for category_fk in brand_categories:
                cat_den = self.parser.filter_df(conditions={'category_fk': category_fk},
                                                data_frame_to_filter=sos_df)[calculation_param].sum()
                filters['category_fk'] = category_fk
                category_df = self.parser.filter_df(conditions=filters, data_frame_to_filter=sos_df)
                cat_num = category_df[calculation_param].sum()
                if cat_num == 0:
                    continue
                cat_res, cat_num, cat_den = self.calculate_sos_res(cat_num, cat_den)
                self.common.write_to_db_result(fk=brand_category_kpi_fk, numerator_id=brand_fk,
                                               context_id=manufacturer_fk,
                                               denominator_id=category_fk, result=cat_res, numerator_result=cat_num,
                                               should_enter=True, denominator_result=cat_den, score=cat_res,
                                               identifier_parent="{}_SOS_brand_{}".format(calculation_type,
                                                                                          str(brand_fk)),
                                               identifier_result="{}_SOS_cat_{}_brand_{}".format(calculation_type,
                                                                                                 str(category_fk),
                                                                                                 str(brand_fk)))
                product_fks = set(self.parser.filter_df(conditions=filters, data_frame_to_filter=sos_df)['product_fk'])
                for sku in product_fks:
                    filters['product_fk'] = sku
                    product_df = self.parser.filter_df(conditions=filters, data_frame_to_filter=sos_df)
                    sku_num = product_df[calculation_param].sum()
                    if sku_num == 0:
                        continue
                    sku_result, sku_num, sku_den = self.calculate_sos_res(sku_num, cat_num)
                    self.common.write_to_db_result(fk=sku_kpi_fk, numerator_id=sku, denominator_id=brand_fk,
                                                   result=sku_result, numerator_result=sku_num, should_enter=True,
                                                   denominator_result=cat_num, score=sku_num,
                                                   context_id=category_fk, weight=manufacturer_fk,
                                                   identifier_parent="{}_SOS_cat_{}_brand_{}".format(calculation_type,
                                                                                                     str(category_fk),
                                                                                                     str(brand_fk)))
                del filters['product_fk']
            del filters['category_fk']
示例#18
0
class GSKSGToolBox:
    KPI_DICT = {
        "planogram": "planogram",
        "secondary_display": "secondary_display",
        "promo": "promo"
    }

    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.common = Common(self.data_provider)
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.manufacturer_fk = None if self.data_provider[Data.OWN_MANUFACTURER]['param_value'].iloc[0] is None else \
            int(self.data_provider[Data.OWN_MANUFACTURER]['param_value'].iloc[0])
        self.set_up_template = pd.read_excel(os.path.join(
            os.path.dirname(os.path.realpath(__file__)), '..', 'Data',
            'gsk_set_up.xlsx'),
                                             sheet_name='Functional KPIs',
                                             keep_default_na=False)

        self.gsk_generator = GSKGenerator(self.data_provider, self.output,
                                          self.common, self.set_up_template)
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.sequence = Sequence(self.data_provider)
        self.set_up_data = {
            ('planogram', Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            ('secondary_display', Const.KPI_TYPE_COLUMN): Const.NO_INFO,
            ('promo', Const.KPI_TYPE_COLUMN): Const.NO_INFO
        }

    def main_calculation(self):
        """
        This function calculates the KPI results.
        """
        # # global kpis in store_level
        assortment_store_dict = self.gsk_generator.availability_store_function(
        )
        self.common.save_json_to_new_tables(assortment_store_dict)
        facings_sos_dict = self.gsk_generator.gsk_global_facings_sos_whole_store_function(
        )
        self.common.save_json_to_new_tables(facings_sos_dict)
        linear_sos_dict = self.gsk_generator.gsk_global_linear_sos_whole_store_function(
        )
        self.common.save_json_to_new_tables(linear_sos_dict)

        # global kpis in category level & kpi results are used for orange score kpi
        assortment_category_dict = self.gsk_generator.availability_category_function(
        )
        self.common.save_json_to_new_tables(assortment_category_dict)
        fsos_category_dict = self.gsk_generator.gsk_global_facings_sos_by_category_function(
        )
        self.common.save_json_to_new_tables(fsos_category_dict)

        # updating the set up dictionary for all local kpis
        for kpi in self.KPI_DICT.keys():
            self.gsk_generator.tool_box.extract_data_set_up_file(
                kpi, self.set_up_data, self.KPI_DICT)

        orange_score_dict = self.orange_score_category(
            assortment_category_dict, fsos_category_dict)

        self.common.save_json_to_new_tables(orange_score_dict)
        self.common.commit_results_data()

        score = 0
        return score

    def msl_compliance_score(self, category, categories_results_json,
                             cat_targets, parent_result_identifier):
        results_list = []
        msl_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.MSL_ORANGE_SCORE)
        msl_categories = self._filter_targets_by_kpi(cat_targets, msl_kpi_fk)
        if category not in categories_results_json:
            dst_result = 0
        else:
            dst_result = categories_results_json[category]
        weight = msl_categories['msl_weight'].iloc[0]
        score = dst_result * weight
        result = score / weight
        results_list.append({
            'fk': msl_kpi_fk,
            'numerator_id': category,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'numerator_result': result,
            'result': result,
            'target': weight,
            'score': score,
            'identifier_parent': parent_result_identifier,
            'should_enter': True
        })
        return score, results_list

    def fsos_compliance_score(self, category, categories_results_json,
                              cat_targets, parent_result_identifier):
        """
               This function return json of keys- categories and values -  kpi result for category
               :param cat_targets-targets df for the specific category
               :param category: pk of category
               :param categories_results_json: type of the desired kpi
               :return category json :  number-category_fk,number-result
           """
        results_list = []
        fsos_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.FSOS_ORANGE_SCORE)
        category_targets = self._filter_targets_by_kpi(cat_targets,
                                                       fsos_kpi_fk)
        dst_result = categories_results_json[
            category] if category in categories_results_json.keys() else 0
        benchmark = category_targets['fsos_benchmark'].iloc[0]
        weight = category_targets['fsos_weight'].iloc[0]
        score = weight if dst_result >= benchmark else 0
        result = score / weight
        results_list.append({
            'fk': fsos_kpi_fk,
            'numerator_id': category,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'numerator_result': result,
            'result': result,
            'target': weight,
            'score': score,
            'identifier_parent': parent_result_identifier,
            'should_enter': True
        })
        return score, results_list

    def extract_json_results_by_kpi(self, general_kpi_results, kpi_type):
        """
            This function return json of keys and values. keys= categories & values = kpi result for category
            :param general_kpi_results: list of json's , each json is a db result
            :param kpi_type: type of the desired kpi
            :return category json :  number-category_fk,number-result
        """
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type)
        if general_kpi_results is None:
            return {}
        categories_results_json = self.extract_json_results(
            kpi_fk, general_kpi_results)
        return categories_results_json

    @staticmethod
    def extract_json_results(kpi_fk, general_kpi_results):
        """
        This function created json of keys- categories and values -  kpi result for category
        :param kpi_fk: pk of the kpi you want to extract results from.
        :param general_kpi_results: list of json's , each json is the db results
        :return category json :  number-category_fk,number-result
        """
        category_json = {}
        for row in general_kpi_results:
            if row['fk'] == kpi_fk:
                category_json[row[
                    DB.SessionResultsConsts.DENOMINATOR_ID]] = row[
                        DB.SessionResultsConsts.RESULT]
        return category_json

    def store_target(self):
        """
        This function filters the external targets df , to the only df with policy that answer current session's store
        attributes.
        It search which store attributes defined the targets policy.
        In addition it gives the targets flexibility to send "changed variables" , external targets need to save
        store param+_key and store_val + _value , than this function search the store param to look for and which value
        it need to have for this policy.
        """
        target_columns = self.targets.columns
        store_att = ['store_name', 'store_number', 'att']
        store_columns = [
            col for col in target_columns
            if len([att for att in store_att if att in col]) > 0
        ]
        for col in store_columns:
            if self.targets.empty:
                return
            if 'key' in col:
                value = col.replace('_key', '') + '_value'
                if value not in store_columns:
                    continue
                self.target_test(col, value)
                store_columns.remove(value)
            else:
                if 'value' in col:
                    continue
                self.target_test(col)

    def target_test(self, store_param, store_param_val=None):
        """
        :param store_param: string , store attribute . by this attribute will compare between targets policy and
        current session
        :param store_param_val: string , if not None the store attribute value the policy have
               This function filters the targets to the only targets with a attributes that answer the current session's
                store attributes
        """
        store_param_val = store_param_val if store_param_val is not None else store_param
        store_param = [
            store_param
        ] if store_param_val is None else self.targets[store_param].unique()
        for param in store_param:
            if param is None:
                continue
            if self.store_info[param][0] is None:
                if self.targets.empty:
                    return
                else:
                    self.targets.drop(self.targets.index, inplace=True)

            self.targets['target_match'] = self.targets[store_param_val].apply(
                self.checking_param, store_info_col=param)
            self.targets = self.targets[self.targets['target_match']]

    def checking_param(self, df_param, store_info_col):
        # x is  self.targets[store_param_val]
        if isinstance(df_param, list):
            if self.store_info[store_info_col][0].encode(
                    GlobalConsts.HelperConsts.UTF8) in df_param:
                return True
        if isinstance(df_param, unicode):
            if self.store_info[store_info_col][0].encode(
                    GlobalConsts.HelperConsts.UTF8
            ) == df_param or df_param == '':
                return True
        if isinstance(df_param, type(None)):
            return True
        return False

    def display_distribution(self, display_name, category_fk, category_targets,
                             parent_identifier, kpi_name, parent_kpi_name,
                             scif_df):
        """
          This Function sum facings of posm that it name contains substring (decided by external_targets )
          if sum facings is equal/bigger than benchmark that gets weight.
            :param display_name display name (in external targets this key contains relevant substrings)
            :param category_fk
            :param category_targets-targets df for the specific category
            :param parent_identifier  - result identifier for this kpi parent
            :param kpi_name - kpi name
            :param parent_kpi_name - this parent kpi name
            :param scif_df - scif filtered by promo activation settings

        """
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name +
                                                    Consts.COMPLIANCE_KPI)
        results_list = []
        identifier_result = self.common.get_dictionary(category_fk=category_fk,
                                                       kpi_fk=kpi_fk)
        weight = category_targets['{}_weight'.format(parent_kpi_name)].iloc[0]

        if scif_df is None:
            results_list.append({
                'fk': kpi_fk,
                'numerator_id': category_fk,
                'denominator_id': self.store_id,
                'denominator_result': 1,
                'numerator_result': 0,
                'result': 0,
                'score': 0,
                'identifier_parent': parent_identifier,
                'identifier_result': identifier_result,
                'target': weight,
                'should_enter': True
            })
            return 0, results_list

        display_products = scif_df[(scif_df['product_type'] == 'POS')
                                   & (scif_df['category_fk'] == category_fk)]

        display_name = "{}_name".format(display_name.lower())
        display_names = category_targets[display_name].iloc[0]
        kpi_result = 0

        # check's if display names (received from external targets) are string or array of strings
        if isinstance(display_names, str) or isinstance(
                display_names, unicode):
            display_array = []
            if len(display_names) > 0:
                display_array.append(display_names)
            display_names = display_array

        # for each display name , search POSM that contains display name (= sub string)
        for display in display_names:
            current_display_prod = display_products[
                display_products['product_name'].str.contains(display)]
            display_sku_level = self.display_sku_results(
                current_display_prod, category_fk, kpi_name)
            kpi_result += current_display_prod['facings'].sum()
            results_list.extend(display_sku_level)

        benchmark = category_targets['{}_benchmark'.format(
            parent_kpi_name)].iloc[0]
        kpi_score = weight if kpi_result >= benchmark else 0
        results_list.append({
            'fk': kpi_fk,
            'numerator_id': category_fk,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'numerator_result': kpi_score,
            'result': kpi_score,
            'score': kpi_score,
            'identifier_parent': parent_identifier,
            'identifier_result': identifier_result,
            'target': weight,
            'should_enter': True
        })
        return kpi_score, results_list

    def display_sku_results(self, display_data, category_fk, kpi_name):
        """
          This Function create for each posm in display data  db result with score of  posm facings.
            :param category_fk
            :param display_data-targets df for the specific category
            :param kpi_name - kpi name
        """
        results_list = []
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name +
                                                    Consts.SKU_LEVEL_LIST)
        parent_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            kpi_name + Consts.COMPLIANCE_KPI)
        identifier_parent = self.common.get_dictionary(category_fk=category_fk,
                                                       kpi_fk=parent_kpi_fk)

        display_names = display_data['item_id'].unique()
        for display in display_names:
            count = float(display_data[display_data['item_id'] == display]
                          ['facings'].sum()) / float(100)
            results_list.append({
                'fk': kpi_fk,
                'numerator_id': display,
                'denominator_id': category_fk,
                'denominator_result': 1,
                'numerator_result': count,
                'result': count,
                'score': count,
                'identifier_parent': identifier_parent,
                'should_enter': True
            })
        return results_list

    def assortment(self):
        """
          This Function get relevant assortment based on filtered scif
        """
        lvl3_assort, filter_scif = self.gsk_generator.tool_box.get_assortment_filtered(
            self.set_up_data, "planogram")
        return lvl3_assort, filter_scif

    def msl_assortment(self, kpi_name):
        """
            :param kpi_name : name of level 3 assortment kpi
            :return kpi_results : data frame of assortment products of the kpi, product's availability,
            product details.(reduce assortment products that are not available)
            filtered by set up
         """
        lvl3_assort, filtered_scif = self.assortment()
        if lvl3_assort is None or lvl3_assort.empty:
            return None
        kpi_assortment_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name)
        kpi_results = lvl3_assort[lvl3_assort['kpi_fk_lvl3'] ==
                                  kpi_assortment_fk]  # general assortment
        kpi_results = pd.merge(kpi_results,
                               self.all_products[Const.PRODUCTS_COLUMNS],
                               how='left',
                               on='product_fk')
        # only distributed products
        kpi_results = kpi_results[kpi_results['in_store'] == 1]

        # filtering substitied products
        kpi_results = kpi_results[
            kpi_results['substitution_product_fk'].isnull()]

        shelf_data = pd.merge(self.match_product_in_scene[[
            'scene_fk', 'product_fk', 'shelf_number'
        ]],
                              filtered_scif[['scene_id', 'product_fk']],
                              how='right',
                              left_on=['scene_fk', 'product_fk'],
                              right_on=['scene_id', 'product_fk'
                                        ])  # why is this happening?

        # merge assortment results with match_product_in_scene for shelf_number parameter
        kpi_results = pd.merge(shelf_data,
                               kpi_results,
                               how='right',
                               on=['product_fk'])  # also problematic
        return kpi_results

    def shelf_compliance(self, category, assortment_df, cat_targets,
                         identifier_parent):
        """
            This function calculate how many assortment products available on specific shelves
            :param category
            :param cat_targets : targets df for the specific category
            :param assortment_df :relevant assortment based on filtered scif
            :param identifier_parent - result identifier for shelf compliance kpi parent .

        """
        results_list = []
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(Consts.SHELF_COMPLIANCE)
        category_targets = self._filter_targets_by_kpi(cat_targets, kpi_fk)
        if assortment_df is not None:
            assortment_cat = assortment_df[assortment_df['category_fk'] ==
                                           category]
            shelf_weight = category_targets['shelf_weight'].iloc[0]
            benchmark = category_targets['shelf_benchmark'].iloc[0]
            shelves = [
                int(shelf) for shelf in
                category_targets['shelf_number'].iloc[0].split(",")
            ]
            shelf_df = assortment_cat[assortment_cat['shelf_number'].isin(
                shelves)]
            numerator = len(shelf_df['product_fk'].unique())
            denominator = len(assortment_cat['product_fk'].unique())
            result = float(numerator) / float(
                denominator) if numerator and denominator != 0 else 0
            score = shelf_weight if result >= benchmark else 0
        else:
            denominator, numerator, score, shelf_weight = 0, 0, 0, 0
            result = float(numerator) / float(
                denominator) if numerator and denominator != 0 else 0
            shelf_weight = category_targets['shelf_weight'].iloc[0]
        results_list.append({
            'fk': kpi_fk,
            'numerator_id': category,
            'denominator_id': self.store_id,
            'denominator_result': denominator,
            'numerator_result': numerator,
            'result': result,
            'target': shelf_weight,
            'score': score,
            'identifier_parent': identifier_parent,
            'should_enter': True
        })
        return score, results_list, shelf_weight

    def planogram(self, category_fk, assortment, category_targets,
                  identifier_parent):
        """
          This function sum sequence kpi and  shelf compliance
            :param category_fk
            :param category_targets : targets df for the specific category
            :param assortment :relevant assortment based on filtered scif
            :param identifier_parent : result identifier for planogram kpi parent .

        """
        results_list = []
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(Consts.PLN_CATEGORY)
        identifier_result = self.common.get_dictionary(category_fk=category_fk,
                                                       kpi_fk=kpi_fk)

        shelf_compliance_score, shelf_compliance_result, shelf_weight = self.shelf_compliance(
            category_fk, assortment, category_targets, identifier_result)
        results_list.extend(shelf_compliance_result)
        sequence_kpi, sequence_weight = self._calculate_sequence(
            category_fk, identifier_result)
        planogram_score = shelf_compliance_score + sequence_kpi
        planogram_weight = shelf_weight + sequence_weight
        planogram_result = planogram_score / float(
            planogram_weight) if planogram_weight else 0
        results_list.append({
            'fk': kpi_fk,
            'numerator_id': category_fk,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'numerator_result': planogram_score,
            'result': planogram_result,
            'target': planogram_weight,
            'score': planogram_score,
            'identifier_parent': identifier_parent,
            'identifier_result': identifier_result,
            'should_enter': True
        })
        return planogram_score, results_list

    def _calculate_sequence(self, cat_fk, planogram_identifier):
        """
        This method calculated the sequence KPIs using the external targets' data and sequence calculation algorithm.
        """
        sequence_kpi_fk, sequence_sku_kpi_fk = self._get_sequence_kpi_fks()
        sequence_targets = self._filter_targets_by_kpi(self.targets,
                                                       sequence_kpi_fk)
        sequence_targets = sequence_targets.loc[sequence_targets.category_fk ==
                                                cat_fk]
        passed_sequences_score, total_sequence_weight = 0, 0
        for i, sequence in sequence_targets.iterrows():
            population, location, sequence_attributes = self._prepare_data_for_sequence_calculation(
                sequence)
            sequence_result = self.sequence.calculate_sequence(
                population, location, sequence_attributes)
            score = self._save_sequence_results_to_db(sequence_sku_kpi_fk,
                                                      sequence_kpi_fk,
                                                      sequence,
                                                      sequence_result)
            passed_sequences_score += score
            total_sequence_weight += sequence[SessionResultsConsts.WEIGHT]
        self._save_sequence_main_level_to_db(sequence_kpi_fk,
                                             planogram_identifier, cat_fk,
                                             passed_sequences_score,
                                             total_sequence_weight)
        return passed_sequences_score, total_sequence_weight

    @staticmethod
    def _prepare_data_for_sequence_calculation(sequence_params):
        """
        This method gets the relevant targets per sequence and returns the sequence params for calculation.
        """
        population = {
            ProductsConsts.PRODUCT_FK:
            sequence_params[ProductsConsts.PRODUCT_FK]
        }
        location = {
            TemplatesConsts.TEMPLATE_GROUP:
            sequence_params[TemplatesConsts.TEMPLATE_GROUP]
        }
        additional_attributes = {
            AdditionalAttr.STRICT_MODE: sequence_params['strict_mode'],
            AdditionalAttr.INCLUDE_STACKING:
            sequence_params['include_stacking'],
            AdditionalAttr.CHECK_ALL_SEQUENCES: True
        }
        return population, location, additional_attributes

    def _extract_target_params(self, sequence_params):
        """
        This method extract the relevant category_fk and result value from the sequence parameters.
        """
        numerator_id = sequence_params[ProductsConsts.CATEGORY_FK]
        result_value = self.ps_data_provider.get_pks_of_result(
            sequence_params['sequence_name'])
        return numerator_id, result_value

    def _save_sequence_main_level_to_db(self, kpi_fk, planogram_identifier,
                                        cat_fk, sequence_score, total_weight):
        """
        This method saves the top sequence level to DB.
        """
        result = round(
            (sequence_score / float(total_weight)), 2) if total_weight else 0
        score = result * total_weight
        self.common.write_to_db_result(fk=kpi_fk,
                                       numerator_id=cat_fk,
                                       numerator_result=sequence_score,
                                       result=result,
                                       denominator_id=self.store_id,
                                       denominator_result=total_weight,
                                       score=score,
                                       weight=total_weight,
                                       target=total_weight,
                                       should_enter=True,
                                       identifier_result=kpi_fk,
                                       identifier_parent=planogram_identifier)

    def _save_sequence_results_to_db(self, kpi_fk, parent_kpi_fk,
                                     sequence_params, sequence_results):
        """
        This method handles the saving of the SKU level sequence KPI.
        :param kpi_fk: Sequence SKU kpi fk.
        :param parent_kpi_fk: Total sequence score kpi fk.
        :param sequence_params: A dictionary with sequence params for the external targets.
        :param sequence_results: A DataFrame with the results that were received by the sequence algorithm.
        :return: The score that was saved (0 or 100 * weight).
        """
        category_fk, result_value = self._extract_target_params(
            sequence_params)
        num_of_sequences = len(sequence_results)
        target, weight = sequence_params[
            SessionResultsConsts.TARGET], sequence_params[
                SessionResultsConsts.WEIGHT]
        score = weight if len(sequence_results) >= target else 0
        self.common.write_to_db_result(fk=kpi_fk,
                                       numerator_id=category_fk,
                                       numerator_result=num_of_sequences,
                                       result=result_value,
                                       denominator_id=self.store_id,
                                       denominator_result=None,
                                       score=score,
                                       weight=weight,
                                       parent_fk=parent_kpi_fk,
                                       target=target,
                                       should_enter=True,
                                       identifier_parent=parent_kpi_fk,
                                       identifier_result=(kpi_fk, category_fk))
        return score

    def _get_sequence_kpi_fks(self):
        """This method fetches the relevant sequence kpi fks"""
        sequence_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.SEQUENCE_KPI)
        sequence_sku_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.SEQUENCE_SKU_KPI)
        return sequence_kpi_fk, sequence_sku_kpi_fk

    def secondary_display(self, category_fk, cat_targets, identifier_parent,
                          scif_df):
        """
            This function calculate secondary score -  0  or full weight if at least
            one of it's child kpis equal to weight.
            :param category_fk
            :param cat_targets : targets df for the specific category
            :param identifier_parent : result identifier for promo activation kpi parent .
            :param scif_df : scif filtered by promo activation settings

        """
        results_list = []
        parent_kpi_name = 'display'
        total_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.DISPLAY_SUMMARY)
        category_targets = self._filter_targets_by_kpi(cat_targets,
                                                       total_kpi_fk)
        weight = category_targets['display_weight'].iloc[0]
        result_identifier = self.common.get_dictionary(category_fk=category_fk,
                                                       kpi_fk=total_kpi_fk)

        dispenser_score, dispenser_res = self.display_distribution(
            Consts.DISPENSER_TARGET, category_fk, category_targets,
            result_identifier, Consts.DISPENSERS, parent_kpi_name, scif_df)
        counter_top_score, counter_top_res = self.display_distribution(
            Consts.COUNTER_TOP_TARGET, category_fk, category_targets,
            result_identifier, Consts.COUNTERTOP, parent_kpi_name, scif_df)
        standee_score, standee_res = self.display_distribution(
            Consts.STANDEE_TARGET, category_fk, category_targets,
            result_identifier, Consts.STANDEE, parent_kpi_name, scif_df)
        results_list.extend(dispenser_res)
        results_list.extend(counter_top_res)
        results_list.extend(standee_res)

        display_score = weight if (dispenser_score == weight) or (
            counter_top_score == weight) or (standee_score == weight) else 0
        results_list.append({
            'fk': total_kpi_fk,
            'numerator_id': category_fk,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'numerator_result': display_score,
            'result': display_score,
            'target': weight,
            'score': display_score,
            'identifier_parent': identifier_parent,
            'identifier_result': result_identifier,
            'should_enter': True
        })

        return results_list, display_score

    def promo_activation(self, category_fk, cat_targets, identifier_parent,
                         scif_df):
        """
            This function calculate promo activation score -  0  or full weight if at least
            one of it's child kpis equal to weight.
            :param category_fk
            :param cat_targets : targets df for the specific category
            :param identifier_parent : result identifier for promo activation kpi parent .
            :param scif_df : scif filtered by promo activation settings

        """
        total_kpi_fk = self.common.get_kpi_fk_by_kpi_type(Consts.PROMO_SUMMARY)
        category_targets = self._filter_targets_by_kpi(cat_targets,
                                                       total_kpi_fk)
        result_identifier = self.common.get_dictionary(category_fk=category_fk,
                                                       kpi_fk=total_kpi_fk)
        results_list = []
        parent_kpi_name = 'promo'
        weight = category_targets['promo_weight'].iloc[0]

        hang_shell_score, hang_shell_res = self.display_distribution(
            Consts.HANGSELL, category_fk, category_targets, result_identifier,
            Consts.HANGSELL_KPI, parent_kpi_name, scif_df)
        top_shelf_score, top_shelf_res = self.display_distribution(
            Consts.TOP_SHELF, category_fk, category_targets, result_identifier,
            Consts.TOP_SHELF_KPI, parent_kpi_name, scif_df)
        results_list.extend(hang_shell_res)
        results_list.extend(top_shelf_res)
        promo_score = weight if (hang_shell_score == weight) or (
            top_shelf_score == weight) else 0

        results_list.append({
            'fk': total_kpi_fk,
            'numerator_id': category_fk,
            'denominator_id': self.store_id,
            'denominator_result': 1,
            'numerator_result': promo_score,
            'result': promo_score,
            'target': weight,
            'score': promo_score,
            'identifier_parent': identifier_parent,
            'identifier_result': result_identifier,
            'should_enter': True
        })

        return results_list, promo_score

    @staticmethod
    def _filter_targets_by_kpi(targets, kpi_fk):
        """ This function filter all targets but targets which related to relevant kpi"""
        filtered_targets = targets.loc[targets.kpi_fk == kpi_fk]
        return filtered_targets

    def orange_score_category(self, assortment_category_res,
                              fsos_category_res):
        """
        This function calculate orange score kpi by category. Settings are based on external targets and set up file.
        :param assortment_category_res :  array  of assortment results
        :param fsos_category_res : array  of facing sos by store results
        """
        results_list = []
        self.store_target()
        if self.targets.empty:
            return

        total_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.ORANGE_SCORE_COMPLIANCE)
        fsos_json_global_results = self.extract_json_results_by_kpi(
            fsos_category_res, Consts.GLOBAL_FSOS_BY_CATEGORY)
        msl_json_global_results = self.extract_json_results_by_kpi(
            assortment_category_res, Consts.GLOBAL_DST_BY_CATEGORY)

        # scif after filtering it by set up file for each kpi
        scif_secondary = self.gsk_generator.tool_box.tests_by_template(
            'secondary_display', self.scif, self.set_up_data)
        scif_promo = self.gsk_generator.tool_box.tests_by_template(
            'promo', self.scif, self.set_up_data)
        categories = self.targets[
            DataProviderConsts.ProductsConsts.CATEGORY_FK].unique()
        assortment = self.msl_assortment('Distribution - SKU')

        for cat in categories:
            orange_score_result_identifier = self.common.get_dictionary(
                category_fk=cat, kpi_fk=total_kpi_fk)

            cat_targets = self.targets[self.targets[
                DataProviderConsts.ProductsConsts.CATEGORY_FK] == cat]

            msl_score, msl_results = self.msl_compliance_score(
                cat, msl_json_global_results, cat_targets,
                orange_score_result_identifier)

            fsos_score, fsos_results = self.fsos_compliance_score(
                cat, fsos_json_global_results, cat_targets,
                orange_score_result_identifier)

            planogram_score, planogram_results = self.planogram(
                cat, assortment, cat_targets, orange_score_result_identifier)

            secondary_display_res, secondary_score = self.secondary_display(
                cat, cat_targets, orange_score_result_identifier,
                scif_secondary)

            promo_activation_res, promo_score = self.promo_activation(
                cat, cat_targets, orange_score_result_identifier, scif_promo)

            compliance_category_score = promo_score + secondary_score + fsos_score + msl_score + planogram_score
            results_list.extend(msl_results + fsos_results +
                                planogram_results + secondary_display_res +
                                promo_activation_res)
            results_list.append({
                'fk':
                total_kpi_fk,
                'numerator_id':
                self.manufacturer_fk,
                'denominator_id':
                cat,
                'denominator_result':
                1,
                'numerator_result':
                compliance_category_score,
                'result':
                compliance_category_score,
                'score':
                compliance_category_score,
                'identifier_result':
                orange_score_result_identifier
            })
        return results_list
class CaseCountCalculator(GlobalSessionToolBox):
    """This class calculates the Case Count SKU KPI set.
    It uses display tags and sub-products in order to calculate results per target and sum
    all of them in order to calculate the main Case Count"""
    def __init__(self, data_provider, common):
        GlobalSessionToolBox.__init__(self, data_provider, None)
        self.filtered_mdis = self._get_filtered_match_display_in_scene()
        self.store_number_1 = self.store_info.store_number_1[0]
        self.filtered_scif = self._get_filtered_scif()
        self.ps_data_provider = PsDataProvider(data_provider)
        self.target = self._get_case_count_targets()
        self.matches = self.get_filtered_matches()
        self.excluded_product_fks = self._get_excluded_product_fks()
        self.adj_graphs_per_scene = {}
        self.common = common

    def _get_case_count_targets(self):
        """
        This method fetches the relevant targets for the case count
        """
        case_count_kpi_fk = self.get_kpi_fk_by_kpi_type(
            Consts.TOTAL_CASES_STORE_KPI)
        targets = self.ps_data_provider.get_kpi_external_targets(
            kpi_fks=[case_count_kpi_fk],
            data_fields=[Src.TARGET],
            key_fields=[Sc.PRODUCT_FK, 'store_number_1'],
            key_filters={'store_number_1': self.store_number_1})
        targets = targets.loc[targets.store_number_1 == self.store_number_1][[
            Pc.PRODUCT_FK, Src.TARGET
        ]]
        return dict(zip(targets[Pc.PRODUCT_FK], targets[Src.TARGET]))

    def _get_filtered_match_display_in_scene(self):
        """ This method filters match display in scene - it saves only "close" and "open" tags"""
        mdis = self.data_provider.match_display_in_scene.loc[
            self.data_provider.match_display_in_scene.display_name.str.
            contains(Consts.RELEVANT_DISPLAYS_SUFFIX)]
        return mdis

    def main_case_count_calculations(self):
        """This method calculates the entire Case Count KPIs set."""
        if not (self.filtered_scif.empty or self.matches.empty):
            try:
                if not self.filtered_mdis.empty:
                    self._prepare_data_for_calculation()
                    self._generate_adj_graphs()
                    facings_res = self._calculate_display_size_facings()
                    sku_cases_res = self._count_number_of_cases()
                    unshoppable_cases_res = self._non_shoppable_case_kpi()
                    implied_cases_res = self._implied_shoppable_cases_kpi()
                else:
                    facings_res = self._calculate_display_size_facings()
                    sku_cases_res = self._count_number_of_cases()
                    unshoppable_cases_res = []
                    implied_cases_res = []
                sku_cases_res = self._remove_nonshoppable_cases_from_shoppable_cases(
                    sku_cases_res, unshoppable_cases_res)
                total_res = self._calculate_total_cases(sku_cases_res +
                                                        implied_cases_res +
                                                        unshoppable_cases_res)
                placeholder_res = self._generate_placeholder_results(
                    facings_res, sku_cases_res, unshoppable_cases_res,
                    implied_cases_res)
                self._save_results_to_db(facings_res + sku_cases_res +
                                         unshoppable_cases_res +
                                         implied_cases_res + total_res +
                                         placeholder_res)
                self._calculate_total_score_level_res(total_res)
            except Exception as err:
                Log.error(
                    "DiageoUS Case Count calculation failed due to the following error: {}"
                    .format(err))

    def _calculate_total_score_level_res(self, total_res_sku_level_results):
        """This method gets the Total Cases SKU level results and aggregates them in order to create the
        mobile store level result"""
        result, kpi_fk = 0, self.get_kpi_fk_by_kpi_type(
            Consts.TOTAL_CASES_STORE_KPI)
        for res in total_res_sku_level_results:
            if res.get(Pc.PRODUCT_FK, 0) in self.target.keys():
                result += res.get(Src.RESULT, 0)
        self.common.write_to_db_result(fk=kpi_fk,
                                       numerator_id=int(self.manufacturer_fk),
                                       result=result,
                                       denominator_id=self.store_id,
                                       identifier_result=kpi_fk)

    def _calculate_total_cases(self, kpi_results):
        """ This method sums # of cases per brand and Implied Shoppable Cases KPIs
        and saves the main result to the DB"""
        kpi_fk = self.get_kpi_fk_by_kpi_type(Consts.TOTAL_CASES_SKU_KPI)
        total_results_per_sku, results_list = Counter(), list()
        for res in kpi_results:
            total_results_per_sku[res[Pc.PRODUCT_FK]] += res[Src.RESULT]
        # save products with targets
        for product_fk, target in self.target.iteritems():
            result = total_results_per_sku.pop(product_fk, 0)
            results_list.append({
                Pc.PRODUCT_FK: product_fk,
                Src.RESULT: result,
                'fk': kpi_fk,
                Src.TARGET: target
            })
        # save products with no targets
        for product_fk in list(
                set(total_results_per_sku.keys()) -
                set(self.excluded_product_fks)):
            result = total_results_per_sku.get(product_fk, 0)
            results_list.append({
                Pc.PRODUCT_FK: product_fk,
                Src.RESULT: result,
                'fk': kpi_fk,
                Src.TARGET: 0
            })
        return results_list

    def _generate_placeholder_results(self, facings_res, sku_cases_res,
                                      unshoppable_cases_res,
                                      implied_cases_res):
        """This method creates KPI results with zeroes to give the illusion that all KPIs were calculated"""
        placeholder_res = []
        total_res = facings_res + sku_cases_res + unshoppable_cases_res + implied_cases_res
        total_product_fks = list(set([res[Sc.PRODUCT_FK]
                                      for res in total_res]))
        for kpi_fk, res_list in [
            (self.get_kpi_fk_by_kpi_type(Consts.FACINGS_KPI), facings_res),
            (self.get_kpi_fk_by_kpi_type(Consts.SHOPPABLE_CASES_KPI),
             sku_cases_res),
            (self.get_kpi_fk_by_kpi_type(Consts.NON_SHOPPABLE_CASES_KPI),
             unshoppable_cases_res),
            (self.get_kpi_fk_by_kpi_type(Consts.IMPLIED_SHOPPABLE_CASES_KPI),
             implied_cases_res)
        ]:
            res_product_fks = [res[Sc.PRODUCT_FK] for res in res_list]
            for missing_product_fk in [
                    fk for fk in total_product_fks if fk not in res_product_fks
            ]:
                placeholder_res.append({
                    Pc.PRODUCT_FK: missing_product_fk,
                    Src.RESULT: 0,
                    'fk': kpi_fk
                })
        return placeholder_res

    @staticmethod
    def _remove_nonshoppable_cases_from_shoppable_cases(
            sku_cases_res, unshoppable_cases_res):
        unshopable_cases_dict = \
            {res[Pc.PRODUCT_FK]: res[Src.RESULT] for res in unshoppable_cases_res if res[Src.RESULT] > 0}
        new_sku_cases_res = []
        for res in sku_cases_res:
            if res[Pc.PRODUCT_FK] in unshopable_cases_dict.keys():
                res[Src.RESULT] = res[Src.RESULT] - unshopable_cases_dict[res[
                    Pc.PRODUCT_FK]]
            new_sku_cases_res.append(res)
        return new_sku_cases_res

    def _save_results_to_db(self, results_list):
        """This method saves the KPI results to DB"""
        total_cases_sku_fk = int(
            self.get_kpi_fk_by_kpi_type(Consts.TOTAL_CASES_SKU_KPI))
        total_cases_store_fk = int(
            self.get_kpi_fk_by_kpi_type(Consts.TOTAL_CASES_STORE_KPI))
        for res in results_list:
            kpi_fk = int(res.get('fk'))
            parent_id = '{}_{}'.format(
                int(res[Pc.PRODUCT_FK]), total_cases_sku_fk
            ) if kpi_fk != total_cases_sku_fk else total_cases_store_fk
            kpi_id = '{}_{}'.format(int(res[Pc.PRODUCT_FK]), kpi_fk)
            result, target = res.get(Src.RESULT), res.get(Src.TARGET)
            score = 1 if target is not None and result >= target else 0
            self.common.write_to_db_result(fk=kpi_fk,
                                           denominator_id=res[Pc.PRODUCT_FK],
                                           result=result,
                                           score=score,
                                           target=target,
                                           identifier_result=kpi_id,
                                           identifier_parent=parent_id,
                                           should_enter=True)

    def _prepare_data_for_calculation(self):
        """This method prepares the data for the case count calculation. Connection between the display
        data and the tagging data."""
        closest_tag_to_display_df = self._calculate_closest_product_to_display(
        )
        closest_tag_to_display_df = self._remove_items_outside_maximum_distance(
            closest_tag_to_display_df)
        self._add_displays_the_closet_product_fk(closest_tag_to_display_df)
        self._add_matches_display_data(closest_tag_to_display_df)
        self._remove_extra_tags_from_case_tags()

    def _remove_extra_tags_from_case_tags(self):
        """This method ensures that if a display is associated with a 'case' tag that no other SKU tags can be
        paired to the same display"""
        case_product_fks = \
            self.scif[self.scif[Sc.SKU_TYPE].isin(['case', 'Case', 'CASE'])][Sc.PRODUCT_FK].unique().tolist()
        display_fks_with_cases = self.matches[
            (self.matches[Consts.DISPLAY_IN_SCENE_FK].notna())
            & (self.matches[Sc.PRODUCT_FK].isin(case_product_fks))][
                Consts.DISPLAY_IN_SCENE_FK].unique().tolist()
        self.matches.loc[(
            (self.matches[Consts.DISPLAY_IN_SCENE_FK].
             isin(display_fks_with_cases)) &
            (~self.matches[Sc.PRODUCT_FK].isin(case_product_fks))),
                         Consts.DISPLAY_IN_SCENE_FK] = pd.np.nan
        return

    @staticmethod
    def _remove_items_outside_maximum_distance(closest_tag_to_display_df):
        """This method removes SKU tags that are too far away from the display tag and limits the number
        of possible tags"""
        max_number_of_tags = 4
        max_distance_from_display_tag = 20000
        closest_tag_to_display_df.sort_values(
            by=['display_in_scene_fk', 'minimum_distance'], inplace=True)
        closest_tag_to_display_df = \
            closest_tag_to_display_df[closest_tag_to_display_df['minimum_distance'] < max_distance_from_display_tag]
        # we need to limit each display to only being associated with up to 4 tags
        closest_tag_to_display_df = closest_tag_to_display_df.groupby(
            'display_in_scene_fk').head(max_number_of_tags)
        return closest_tag_to_display_df

    def _calculate_closest_product_to_display(self):
        """This method calculates the closest tag for each display and returns a DataFrame with the results"""
        matches = self.matches[self.matches[Mc.SCENE_FK].isin(
            self._get_scenes_with_relevant_displays())]
        matches = matches[Consts.RLV_FIELDS_FOR_MATCHES_CLOSET_DISPLAY_CALC]
        mdis = self.filtered_mdis[
            Consts.RLV_FIELDS_FOR_DISPLAY_IN_SCENE_CLOSET_TAG_CALC]
        closest_display_data = matches.apply(
            lambda row: self._apply_closet_point_logic_on_row(row, mdis, 'pk'),
            axis=1)
        closest_display_data = pd.DataFrame(
            closest_display_data.values.tolist())
        return closest_display_data

    def _calculate_display_size_facings(self):
        """This method calculates the number of facings for SKUs with the relevant SKU Type
        only in scenes that have displays"""
        filtered_scif = self.filtered_scif.loc[(
            self.filtered_scif[Sc.SKU_TYPE].isin(Consts.FACINGS_SKU_TYPES))]
        filtered_scif = filtered_scif.loc[filtered_scif[Sc.TAGGED] > 0][[
            Pc.SUBSTITUTION_PRODUCT_FK, Sc.TAGGED
        ]]
        results_df = filtered_scif.groupby(
            Pc.SUBSTITUTION_PRODUCT_FK, as_index=False).sum().rename(
                {
                    Sc.TAGGED: Src.RESULT,
                    Sc.SUBSTITUTION_PRODUCT_FK: Sc.PRODUCT_FK
                },
                axis=1)
        results_df = results_df.merge(pd.DataFrame(
            {Pc.PRODUCT_FK: self.target.keys()}),
                                      how='outer',
                                      on=Pc.PRODUCT_FK)
        results_df = results_df.assign(
            fk=self.get_kpi_fk_by_kpi_type(Consts.FACINGS_KPI))
        results_df = results_df.fillna(0)
        return results_df.to_dict('records')

    def _get_results_for_branded_other_cases(self):
        """This method identifies branded other cases and attempts to distribute them across the products in the
        open case most closely above the branded other case"""
        results = []
        for scene_fk in self._get_scenes_with_relevant_displays():
            adj_g = self.adj_graphs_per_scene[scene_fk]
            branded_other_cases = self.matches[
                (self.matches['product_type'] == 'Other')
                & (self.matches[Consts.MPIPS_FK] == Consts.PACK_FK) &
                (self.matches['scene_fk'] == scene_fk)]
            branded_other_match_fks = branded_other_cases.scene_match_fk.tolist(
            )
            for node, node_data in adj_g.nodes(data=True):
                node_scene_match_fks = list(node_data['match_fk'].values)
                if any(match in node_scene_match_fks
                       for match in branded_other_match_fks):
                    results.append(self._find_open_case_above(node, adj_g))
        return results

    def _find_open_case_above(self, node, adj_g):
        node_data = adj_g.nodes[node]
        root_brand_name = list(node_data['brand_name'].values)
        paths = self._get_relevant_path_for_calculation(adj_g)
        paths = [path for path in paths if node in path]
        activated = False
        case_data = None
        result = list(node_data['substitution_product_fk'].values)
        for case in paths[0]:
            if case == node:
                activated = True
            if not activated:
                continue  # we haven't reached the brand-other case in the path yet
            if not self._get_case_status(adj_g.nodes[case]):
                continue  # the case is closed, and we haven't reached the first open case yet
            else:
                case_data = adj_g.nodes[case]
                break

        if case_data:
            case_brand_names = list(case_data['brand_name'].values)
            if any(brand in root_brand_name for brand in case_brand_names):
                result = list(case_data['substitution_product_fk'].values)

        return result
示例#20
0
class TWEGAUSceneToolBox:
    def __init__(self, data_provider, output, common):
        self.output = output
        self.data_provider = data_provider
        self.common = common
        self.templates_path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), '..', 'Data')
        self.excel_file_path = os.path.join(self.templates_path,
                                            'Template.xlsx')
        self.project_name = self.data_provider.project_name
        self.session_uid = self.data_provider.session_uid
        self.products = self.data_provider[Data.PRODUCTS]
        self.templates = self.data_provider[Data.TEMPLATES]
        self.all_products = self.data_provider[Data.ALL_PRODUCTS]
        self.scif = self.data_provider.scene_item_facts
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.session_info = self.data_provider[Data.SESSION_INFO]
        self.scene_info = self.data_provider[Data.SCENES_INFO]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_id = self.store_info.iloc[0].store_fk
        self.store_type = self.data_provider.store_type
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.kpi_static_data = self.common.get_kpi_static_data()
        self.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.targets = self.ps_data_provider.get_kpi_external_targets()
        self.empty_product_ids = self.all_products.query(
            'product_name.str.contains("empty", case=False) or'
            ' product_name.str.contains("irrelevant", case=False)',
            engine='python')['product_fk'].values

    def _get_zone_based_data(self, kpi, kpi_sheet_row, denominator_row):
        # generate scene max shelf max bay map
        zone_number = kpi_sheet_row[ZONE_NAME]
        shelves_policy_from_top = [
            int(x.strip())
            for x in str(kpi_sheet_row[SHELF_POLICY_FROM_TOP]).split(',')
            if x.strip()
        ]
        permitted_shelves = [
            int(x.strip())
            for x in str(kpi_sheet_row[NUMBER_OF_SHELVES]).split(',')
            if x.strip()
        ]
        Log.info(
            "Calulating for zone {z} with shelf policy {sh} and permitted shelves {psh}"
            .format(z=zone_number,
                    sh=shelves_policy_from_top,
                    psh=permitted_shelves))
        unique_manufacturer_products_count = 0
        # DENOMINATOR
        if not denominator_row.empty:
            denominator_filters, denominator_filter_string = get_filter_string_per_row(
                denominator_row,
                DENOMINATOR_FILTER_ENTITIES,
            )
            unique_manufacturer_products_count = len(
                self.scif.query(denominator_filter_string).product_fk.unique())
        if not is_nan(kpi_sheet_row[STORE_TYPE]):
            if bool(kpi_sheet_row[STORE_TYPE].strip()
                    ) and kpi_sheet_row[STORE_TYPE].strip().lower() != 'all':
                Log.info("Check the store types in excel...")
                permitted_store_types = [
                    x.strip() for x in kpi_sheet_row[STORE_TYPE].split(',')
                    if x.strip()
                ]
                if self.store_info.store_type.values[
                        0] not in permitted_store_types:
                    Log.info(
                        "Store type = {st} not permitted for session {ses}...".
                        format(st=self.store_info.store_type.values[0],
                               ses=self.session_uid))
                    return []
        filters, filter_string = get_filter_string_per_row(
            kpi_sheet_row,
            ZONE_NUMERATOR_FILTER_ENTITIES,
            additional_filters=ZONE_ADDITIONAL_FILTERS_PER_COL,
        )
        Log.info("Store filters = {ft} and filter_string = {fts}...".format(
            ft=filters, fts=filter_string))
        # combined tables
        match_product_df = pd.merge(self.match_product_in_scene,
                                    self.products,
                                    how='left',
                                    left_on=['product_fk'],
                                    right_on=['product_fk'])

        scene_template_df = pd.merge(self.scene_info,
                                     self.templates,
                                     how='left',
                                     left_on=['template_fk'],
                                     right_on=['template_fk'])

        product_scene_join_data = pd.merge(match_product_df,
                                           scene_template_df,
                                           how='left',
                                           left_on=['scene_fk'],
                                           right_on=['scene_fk'])
        if filters:
            filtered_grouped_scene_items = product_scene_join_data.query(filter_string) \
                .groupby(filters, as_index=False)
        else:
            # dummy structure without filters
            filtered_grouped_scene_items = [
                ('', product_scene_join_data.query(filter_string))
            ]
        # get the scene_id's worth getting data from
        scene_data_map = defaultdict(list)
        for each_group_by_manf_templ in filtered_grouped_scene_items:
            # append scene to group by
            scene_type_grouped_by_scene = each_group_by_manf_templ[1].groupby(
                SCENE_FK)
            for scene_id, scene_data in scene_type_grouped_by_scene:
                exclude_items = False
                valid_bay_numbers = self.get_valid_bay_numbers(
                    scene_id, permitted_shelves)
                if not valid_bay_numbers:
                    continue
                scene_per_bay_number = scene_data.query(
                    'shelf_number in {shelves} and bay_number in {bays}'.
                    format(shelves=shelves_policy_from_top,
                           bays=valid_bay_numbers)).groupby(['bay_number'])
                items_to_check_str = None
                if not is_nan(kpi_sheet_row.exclude_include_policy):
                    match_exclude = exclude_re.search(
                        kpi_sheet_row.exclude_include_policy)
                    if not match_exclude:
                        match_only = only_re.search(
                            kpi_sheet_row.exclude_include_policy)
                        items_to_check_str = match_only.groups()[-1]
                        exclude_items = False
                    else:
                        items_to_check_str = match_exclude.groups()[-1]
                        exclude_items = True
                # the deciding loop
                # bay iterator
                for bay_number, scene_data_per_bay in scene_per_bay_number:
                    total_products = []  # contain the total products per bay
                    if scene_data_per_bay.empty:
                        return {}
                    bay_number = bay_number
                    scene_data_per_bay_shelf = scene_data_per_bay.groupby(
                        'shelf_number')
                    denominator_entity_id = EXCEL_ENTITY_MAP[
                        kpi_sheet_row.denominator_fk]
                    denominator_data = getattr(
                        scene_data_per_bay,
                        EXCEL_DB_MAP[kpi_sheet_row.denominator_fk],
                        pd.Series())
                    if denominator_data.empty:
                        # find in self
                        denominator_id = getattr(
                            self, EXCEL_DB_MAP[kpi_sheet_row.denominator_fk],
                            None)
                    else:
                        denominator_id = denominator_data.unique()[0]
                    # shelf iterator
                    for shelf_id, shelf_data in scene_data_per_bay_shelf:
                        if items_to_check_str:
                            # exclude/include logic
                            last_shelf_number = str(
                                shelf_data.n_shelf_items.unique()[0])
                            shelf_filter = items_to_check_str.replace(
                                'N', last_shelf_number)
                            shelf_filter_string = '[{}]'.format(shelf_filter)
                            if exclude_items:
                                # exclude rows in `items_to_check_tuple`
                                required_shelf_items = shelf_data.drop(
                                    shelf_data.query(
                                        'facing_sequence_number in {filter_string}'
                                        .format(
                                            filter_string=shelf_filter_string
                                        )).index.tolist())
                            else:
                                # it is include_only:
                                required_shelf_items = shelf_data.query(
                                    'facing_sequence_number in {filter_string}'
                                    .format(filter_string=shelf_filter_string))
                            product_ids = required_shelf_items.product_fk.tolist(
                            )
                        else:
                            product_ids = shelf_data.product_fk.tolist()
                        total_products.extend(product_ids)
                    # prod_count_map is per bay and shelf
                    scene_data_map[scene_id].append({
                        'fk':
                        int(kpi['pk']),
                        'store':
                        denominator_id,
                        'product_count_map':
                        Counter(total_products),
                        'bay_number':
                        bay_number,
                        'kpi_name':
                        kpi_sheet_row[KPI_NAME],
                        'zone_number':
                        zone_number,
                        'unique_manufacturer_products_count':
                        unique_manufacturer_products_count,
                    })
        return scene_data_map

    def get_template_details(self, sheet_name):
        template = pd.read_excel(self.excel_file_path, sheetname=sheet_name)
        return template

    def calculate_zone_kpis(self):
        Log.info("Scene based calculations for zone KPIs...")
        zone_kpi_sheet = self.get_template_details(ZONE_KPI_SHEET)
        name_grouped_zone_kpi_sheet = zone_kpi_sheet.groupby(KPI_TYPE)
        for each_kpi in name_grouped_zone_kpi_sheet:
            # ugly hack! -- this came as a requirement after the kpi was written
            each_kpi_type = "SCENE_" + each_kpi[
                0]  # manipulate kpi name to fit into its scene based
            kpi_sheet_rows = each_kpi[1]
            denominator_row = pd.Series()
            kpi = self.kpi_static_data[
                (self.kpi_static_data[KPI_FAMILY] == PS_KPI_FAMILY)
                & (self.kpi_static_data[TYPE] == each_kpi_type)
                & (self.kpi_static_data['delete_time'].isnull())]
            if kpi.empty:
                Log.info("KPI Name:{} not found in DB".format(each_kpi_type))
            else:
                Log.info("KPI Name:{} found in DB".format(each_kpi_type))
                if 'sku_all' not in each_kpi_type.lower():
                    # Skipping zone KPI's for Mobile Reports.
                    continue
                list_of_zone_data = []
                for idx, each_kpi_sheet_row in kpi_sheet_rows.iterrows():
                    zone_data = self._get_zone_based_data(
                        kpi,
                        each_kpi_sheet_row.T,
                        denominator_row=denominator_row)
                    if zone_data:
                        # empty when the row in sheet could not find any relevant data for zone
                        list_of_zone_data.append(zone_data)
                # write for products
                for each_scene_zone_map in list_of_zone_data:
                    for scene_id, bay_zone_list in each_scene_zone_map.iteritems(
                    ):
                        for zone_data in bay_zone_list:
                            product_counter = zone_data['product_count_map']
                            for prod_id, count in product_counter.iteritems():
                                Log.info(
                                    "Product_id is {pr} and count is {cn} on bay {bay}"
                                    .format(pr=prod_id,
                                            cn=count,
                                            bay=zone_data['bay_number']))
                                if int(prod_id) not in self.empty_product_ids:
                                    in_assort_sc_values = self.scif.query(
                                        "item_id=={prod_id}".format(
                                            prod_id=prod_id)).in_assort_sc
                                    in_assort_sc = 0
                                    if not in_assort_sc_values.empty:
                                        if not is_nan(
                                                in_assort_sc_values.values[0]):
                                            in_assort_sc = int(
                                                in_assort_sc_values.values[0])
                                    self.common.write_to_db_result(
                                        fk=int(zone_data['fk']),
                                        numerator_id=int(
                                            prod_id),  # product ID
                                        numerator_result=int(
                                            zone_data['bay_number']),
                                        # bay number comes as numerator
                                        denominator_id=int(
                                            zone_data['store']),  # store ID
                                        denominator_result=int(
                                            scene_id
                                        ),  # scene id comes as denominator
                                        result=int(count),  # save the count
                                        score=in_assort_sc,
                                        context_id=int(
                                            zone_data['zone_number']),
                                        by_scene=True)

    def get_shelf_limit_for_scene(self, scene_id):
        shelf_limit_per_scene_map = defaultdict(list)
        scene_data = self.match_product_in_scene.loc[
            self.match_product_in_scene['scene_fk'] == scene_id]
        _bay_grouped_scene_data = scene_data.groupby('bay_number',
                                                     as_index=False)
        for each_bay in _bay_grouped_scene_data:
            bay_number = each_bay[0]
            scene_data = each_bay[1]
            if not scene_data.empty:
                shelf_limit_per_scene_map[scene_id].append((bay_number, {
                    'max_shelf':
                    scene_data[SHELF_NUMBER].max(),
                    'min_shelf':
                    scene_data[SHELF_NUMBER].min()
                }))
        return shelf_limit_per_scene_map

    def get_valid_bay_numbers(self, scene_id, permitted_shelves):
        scene_max_min_map = self.get_shelf_limit_for_scene(scene_id)
        bay_numbers = []
        for scene_id, bay_shelf_map in scene_max_min_map.iteritems():
            for each_map in bay_shelf_map:
                _bay_number = each_map[0]
                scene_max_min_map = each_map[1]
                for each_permitted_shelf in permitted_shelves:
                    if scene_max_min_map['max_shelf'] == each_permitted_shelf:
                        bay_numbers.append(_bay_number)
        return bay_numbers