예제 #1
0
class LIBERTYToolBox:

    def __init__(self, data_provider, output, common_db):
        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.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.store_info = self.ps_data_provider.get_ps_store_info(self.data_provider[Data.STORE_INFO])
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.scif = self.scif[self.scif['product_type'] != "Irrelevant"]
        self.templates = {}
        self.result_values = self.ps_data_provider.get_result_values()
        for sheet in Const.SHEETS:
            self.templates[sheet] = pd.read_excel(Const.TEMPLATE_PATH, sheetname=sheet).fillna('')
        self.common_db = common_db
        self.survey = Survey(self.data_provider, output=self.output, ps_data_provider=self.ps_data_provider,
                             common=self.common_db)
        self.manufacturer_fk = Const.MANUFACTURER_FK
        self.region = self.store_info['region_name'].iloc[0]
        self.store_type = self.store_info['store_type'].iloc[0]
        self.retailer = self.store_info['retailer_name'].iloc[0]
        self.branch = self.store_info['branch_name'].iloc[0]
        self.additional_attribute_4 = self.store_info['additional_attribute_4'].iloc[0]
        self.additional_attribute_7 = self.store_info['additional_attribute_7'].iloc[0]
        self.body_armor_delivered = self.get_body_armor_delivery_status()

    # main functions:

    def main_calculation(self, *args, **kwargs):
        """
            This function gets all the scene results from the SceneKPI, after that calculates every session's KPI,
            and in the end it calls "filter results" to choose every KPI and scene and write the results in DB.
        """
        red_score = 0
        main_template = self.templates[Const.KPIS]
        for i, main_line in main_template.iterrows():
            relevant_store_types = self.does_exist(main_line, Const.ADDITIONAL_ATTRIBUTE_7)
            if relevant_store_types and self.additional_attribute_7 not in relevant_store_types:
                continue
            result = self.calculate_main_kpi(main_line)
            if result:
                red_score += main_line[Const.WEIGHT]

        if len(self.common_db.kpi_results) > 0:
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(Const.RED_SCORE_PARENT)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=1, denominator_id=self.store_id, result=red_score,
                                              identifier_result=Const.RED_SCORE_PARENT, should_enter=True)
        return

    def calculate_main_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        relevant_scif = self.scif
        scene_types = self.does_exist(main_line, Const.SCENE_TYPE)
        if scene_types:
            relevant_scif = relevant_scif[relevant_scif['template_name'].isin(scene_types)]
        excluded_scene_types = self.does_exist(main_line, Const.EXCLUDED_SCENE_TYPE)
        if excluded_scene_types:
            relevant_scif = relevant_scif[~relevant_scif['template_name'].isin(excluded_scene_types)]
        template_groups = self.does_exist(main_line, Const.TEMPLATE_GROUP)
        if template_groups:
            relevant_scif = relevant_scif[relevant_scif['template_group'].isin(template_groups)]

        result = self.calculate_kpi_by_type(main_line, relevant_scif)

        return result

    def calculate_kpi_by_type(self, main_line, relevant_scif):
        """
        the function calculates all the kpis
        :param main_line: one kpi line from the main template
        :param relevant_scif:
        :return: boolean, but it can be None if we want not to write it in DB
        """
        kpi_type = main_line[Const.KPI_TYPE]
        relevant_template = self.templates[kpi_type]
        kpi_line = relevant_template[relevant_template[Const.KPI_NAME] == main_line[Const.KPI_NAME]].iloc[0]
        kpi_function = self.get_kpi_function(kpi_type)
        weight = main_line[Const.WEIGHT]

        if relevant_scif.empty:
            result = 0
        else:
            result = kpi_function(kpi_line, relevant_scif, weight)

        result_type_fk = self.ps_data_provider.get_pks_of_result(
            Const.PASS) if result > 0 else self.ps_data_provider.get_pks_of_result(Const.FAIL)

        kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(kpi_name)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=result_type_fk, identifier_parent=Const.RED_SCORE_PARENT,
                                          identifier_result=kpi_name, should_enter=True)

        return result

    # SOS functions
    def calculate_sos(self, kpi_line, relevant_scif, weight):
        market_share_required = self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET)
        if market_share_required:
            market_share_target = self.get_market_share_target()
        else:
            market_share_target = 0

        if not market_share_target:
            market_share_target = 0

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            relevant_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        number_of_facings = relevant_scif['facings'].sum()
        result = 1 if number_of_facings > market_share_target else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=number_of_facings, target=market_share_target,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    # Availability functions
    def calculate_availability(self, kpi_line, relevant_scif, weight):
        survey_question_skus_required = self.does_exist(kpi_line, Const.SURVEY_QUESTION_SKUS_REQUIRED)
        if survey_question_skus_required:
            survey_question_skus = self.get_relevant_product_assortment_by_kpi_name(kpi_line[Const.KPI_NAME])
            unique_skus = \
                relevant_scif[relevant_scif['product_fk'].isin(survey_question_skus)]['product_fk'].unique().tolist()
        else:
            manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
            if manufacturer:
                relevant_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]
            brand = self.does_exist(kpi_line, Const.BRAND)
            if brand:
                relevant_scif = relevant_scif[relevant_scif['brand_name'].isin(brand)]
            category = self.does_exist(kpi_line, Const.CATEGORY)
            if category:
                relevant_scif = relevant_scif[relevant_scif['category'].isin(category)]
            excluded_brand = self.does_exist(kpi_line, Const.EXCLUDED_BRAND)
            if excluded_brand:
                relevant_scif = relevant_scif[~relevant_scif['brand_name'].isin(excluded_brand)]
            unique_skus = relevant_scif['product_fk'].unique().tolist()

        length_of_unique_skus = len(unique_skus)
        minimum_number_of_skus = kpi_line[Const.MINIMUM_NUMBER_OF_SKUS]

        result = 1 if length_of_unique_skus >= minimum_number_of_skus else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=length_of_unique_skus, target=minimum_number_of_skus,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    def get_relevant_product_assortment_by_kpi_name(self, kpi_name):
        template = self.templates[Const.SURVEY_QUESTION_SKUS]
        relevant_template = template[template[Const.KPI_NAME] == kpi_name]
        relevant_ean_codes = relevant_template[Const.EAN_CODE].unique().tolist()
        relevant_ean_codes = [str(int(x)) for x in relevant_ean_codes if x != '']  # we need this to fix dumb template
        relevant_products = self.all_products[self.all_products['product_ean_code'].isin(relevant_ean_codes)]
        return relevant_products['product_fk'].unique().tolist()

    # Count of Display functions
    def calculate_count_of_display(self, kpi_line, relevant_scif, weight):
        filtered_scif = relevant_scif

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        brand = self.does_exist(kpi_line, Const.BRAND)
        if brand:
            filtered_scif = filtered_scif[filtered_scif['brand_name'].isin(brand)]

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            filtered_scif = filtered_scif[filtered_scif['att4'].isin(ssd_still)]

        size_subpackages = self.does_exist(kpi_line, Const.SIZE_SUBPACKAGES_NUM)
        if size_subpackages:
            # convert all pairings of size and number of subpackages to tuples
            size_subpackages_tuples = [tuple([float(i) for i in x.split(';')]) for x in size_subpackages]
            filtered_scif = filtered_scif[pd.Series(list(zip(filtered_scif['size'],
                                                             filtered_scif['number_of_sub_packages'])),
                                                    index=filtered_scif.index).isin(size_subpackages_tuples)]

        sub_packages = self.does_exist(kpi_line, Const.SUBPACKAGES_NUM)
        if sub_packages:
            if sub_packages == [Const.NOT_NULL]:
                filtered_scif = filtered_scif[~filtered_scif['number_of_sub_packages'].isnull()]
            else:
                filtered_scif = filtered_scif[filtered_scif['number_of_sub_packages'].isin([int(i) for i in sub_packages])]

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            number_of_passing_displays = self.get_number_of_passing_displays(filtered_scif)

            parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                              denominator_id=self.store_id, denominator_result=0, weight=weight,
                                              result=number_of_passing_displays,
                                              identifier_parent=parent_kpi_name, should_enter=True)
            return 1 if number_of_passing_displays > 0 else 0
        else:
            return 0

    # Share of Display functions
    def calculate_share_of_display(self, kpi_line, relevant_scif, weight):
        filtered_scif = relevant_scif

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            filtered_scif = filtered_scif[filtered_scif['att4'].isin(ssd_still)]

        if self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET):
            market_share_target = self.get_market_share_target(ssd_still=ssd_still)
        else:
            market_share_target = 0

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            number_of_passing_displays = self.get_number_of_passing_displays(filtered_scif)

            result = 1 if number_of_passing_displays > market_share_target else 0

            parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                              denominator_id=self.store_id, denominator_result=0, weight=weight,
                                              result=number_of_passing_displays, target=market_share_target,
                                              identifier_parent=parent_kpi_name, should_enter=True)

            return result
        else:
            return 0

    def get_number_of_passing_displays(self, filtered_scif):
        if filtered_scif.empty:
            return 0

        filtered_scif['passed_displays'] = \
            filtered_scif.apply(lambda row: self._calculate_pass_status_of_display(row), axis=1)

        return filtered_scif['passed_displays'].sum()

    def _calculate_pass_status_of_display(self, row):  # need to move to external KPI targets
        template = self.templates[Const.MINIMUM_FACINGS]
        package_category = (row['size'], row['number_of_sub_packages'], row['size_unit'])
        relevant_template = template[pd.Series(zip(template['size'],
                                                   template['subpackages_num'],
                                                   template['unit_of_measure'])) == package_category]
        minimum_facings = relevant_template[Const.MINIMUM_FACINGS_REQUIRED_FOR_DISPLAY].min()
        return 1 if row['facings'] > minimum_facings else 0

    # Survey functions
    def calculate_survey(self, kpi_line, relevant_scif, weight):
        return 1 if self.survey.check_survey_answer(kpi_line[Const.QUESTION_TEXT], 'Yes') else 0

    # helper functions
    def get_market_share_target(self, ssd_still=None):  # need to move to external KPI targets
        template = self.templates[Const.MARKET_SHARE]
        relevant_template = template[(template[Const.ADDITIONAL_ATTRIBUTE_4] == self.additional_attribute_4) &
                                     (template[Const.RETAILER] == self.retailer) &
                                     (template[Const.BRANCH] == self.branch)]

        if relevant_template.empty:
            if ssd_still:
                if ssd_still[0].lower() == Const.SSD.lower():
                    return 49
                elif ssd_still[0].lower() == Const.STILL.lower():
                    return 16
                else:
                    return 0
            else:
                return 26

        if ssd_still:
            if ssd_still[0].lower() == Const.SSD.lower():
                return relevant_template[Const.SSD].iloc[0]
            elif ssd_still[0].lower() == Const.STILL.lower():
                return relevant_template[Const.STILL].iloc[0]

        # total 26, ssd only 49, still only 16
        return relevant_template[Const.SSD_AND_STILL].iloc[0]

    def get_body_armor_delivery_status(self):
        if self.store_info['additional_attribute_8'].iloc[0] == 'Y':
            return True
        else:
            return False

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.COUNT_OF_DISPLAY:
            return self.calculate_count_of_display
        elif kpi_type == Const.SHARE_OF_DISPLAY:
            return self.calculate_share_of_display
        elif kpi_type == Const.SURVEY:
            return self.calculate_survey
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized".format(kpi_type))
            return None

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float]:
                return [cell]
            elif type(cell) in [unicode, str]:
                return [x.strip() for x in cell.split(",")]
        return None
예제 #2
0
class LIBERTYToolBox:

    def __init__(self, data_provider, output, common_db):
        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.ps_data_provider = PsDataProvider(self.data_provider, self.output)
        self.store_info = self.ps_data_provider.get_ps_store_info(
            self.data_provider[Data.STORE_INFO])
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.scif = self.scif[self.scif['product_type'] != "Irrelevant"]
        self.result_values = self.ps_data_provider.get_result_values()
        self.templates = self.read_templates()
        self.common_db = common_db
        self.survey = Survey(self.data_provider, output=self.output, ps_data_provider=self.ps_data_provider,
                             common=self.common_db)
        self.manufacturer_fk = Const.MANUFACTURER_FK
        self.region = self.store_info['region_name'].iloc[0]
        self.store_type = self.store_info['store_type'].iloc[0]
        self.retailer = self.store_info['retailer_name'].iloc[0]
        self.branch = self.store_info['branch_name'].iloc[0]
        self.additional_attribute_4 = self.store_info['additional_attribute_4'].iloc[0]
        self.additional_attribute_7 = self.store_info['additional_attribute_7'].iloc[0]
        self.body_armor_delivered = self.get_body_armor_delivery_status()
        self.convert_base_size_and_multi_pack()

    def read_templates(self):
        templates = {}
        for sheet in Const.SHEETS:
            converters = None
            if sheet == Const.MINIMUM_FACINGS:
                converters = {Const.BASE_SIZE_MIN: self.convert_base_size_values,
                              Const.BASE_SIZE_MAX: self.convert_base_size_values}
            templates[sheet] = \
                pd.read_excel(Const.TEMPLATE_PATH, sheet_name=sheet,
                              converters=converters).fillna('')
        return templates

    # main functions:

    def main_calculation(self, *args, **kwargs):
        """
            This function gets all the scene results from the SceneKPI, after that calculates every session's KPI,
            and in the end it calls "filter results" to choose every KPI and scene and write the results in DB.
        """
        if self.region != 'Liberty':
            return
        red_score = 0
        main_template = self.templates[Const.KPIS]
        for i, main_line in main_template.iterrows():
            relevant_store_types = self.does_exist(main_line, Const.ADDITIONAL_ATTRIBUTE_7)
            if relevant_store_types and self.additional_attribute_7 not in relevant_store_types:
                continue
            result = self.calculate_main_kpi(main_line)
            if result:
                red_score += main_line[Const.WEIGHT] * result

        if len(self.common_db.kpi_results) > 0:
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(Const.RED_SCORE_PARENT)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=1, denominator_id=self.store_id, result=red_score,
                                              identifier_result=Const.RED_SCORE_PARENT, should_enter=True)
        return

    def calculate_main_kpi(self, main_line):
        """
        This function gets a line from the main_sheet, transfers it to the match function, and checks all of the
        KPIs in the same name in the match sheet.
        :param main_line: series from the template of the main_sheet.
        """
        relevant_scif = self.scif
        scene_types = self.does_exist(main_line, Const.SCENE_TYPE)
        if scene_types:
            relevant_scif = relevant_scif[relevant_scif['template_name'].isin(scene_types)]
        excluded_scene_types = self.does_exist(main_line, Const.EXCLUDED_SCENE_TYPE)
        if excluded_scene_types:
            relevant_scif = relevant_scif[~relevant_scif['template_name'].isin(
                excluded_scene_types)]
        template_groups = self.does_exist(main_line, Const.TEMPLATE_GROUP)
        if template_groups:
            relevant_scif = relevant_scif[relevant_scif['template_group'].isin(template_groups)]

        result = self.calculate_kpi_by_type(main_line, relevant_scif)

        return result

    def calculate_kpi_by_type(self, main_line, relevant_scif):
        """
        the function calculates all the kpis
        :param main_line: one kpi line from the main template
        :param relevant_scif:
        :return: boolean, but it can be None if we want not to write it in DB
        """
        kpi_type = main_line[Const.KPI_TYPE]
        relevant_template = self.templates[kpi_type]
        kpi_line = relevant_template[relevant_template[Const.KPI_NAME]
                                     == main_line[Const.KPI_NAME]].iloc[0]
        kpi_function = self.get_kpi_function(kpi_type)
        weight = main_line[Const.WEIGHT]

        if relevant_scif.empty:
            result = 0
        else:
            result = kpi_function(kpi_line, relevant_scif, weight)

        result_type_fk = self.ps_data_provider.get_pks_of_result(
            Const.PASS) if result > 0 else self.ps_data_provider.get_pks_of_result(Const.FAIL)

        if self.does_exist(main_line, Const.PARENT_KPI_NAME):
            # if this is a child KPI, we do not need to return a value to the Total Score KPI
            return 0
        else:  # normal behavior for when this isn't a child KPI
            kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(kpi_name)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                              denominator_id=self.store_id, denominator_result=0, weight=weight,
                                              result=result_type_fk, identifier_parent=Const.RED_SCORE_PARENT,
                                              identifier_result=kpi_name, should_enter=True)
            return result

    # SOS functions

    def calculate_sos(self, kpi_line, relevant_scif, weight):
        market_share_required = self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET)
        if market_share_required:
            market_share_target = self.get_market_share_target()
        else:
            market_share_target = 0

        if not market_share_target:
            market_share_target = 0

        denominator_facings = relevant_scif['facings'].sum()

        filtered_scif = relevant_scif.copy()

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
        if liberty_truck:
            liberty_truck_scif = relevant_scif[relevant_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                liberty_truck)]
            filtered_scif = filtered_scif.append(liberty_truck_scif, sort=False).drop_duplicates()

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        numerator_facings = filtered_scif['facings'].sum()
        sos_value = numerator_facings / float(denominator_facings)
        result = 1 if sos_value > market_share_target else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=numerator_facings,
                                          denominator_id=self.store_id, denominator_result=denominator_facings,
                                          weight=weight, score=result * weight,
                                          result=sos_value * 100, target=market_share_target * 100,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    # Availability functions
    def calculate_availability(self, kpi_line, relevant_scif, weight):
        survey_question_skus_required = self.does_exist(
            kpi_line, Const.SURVEY_QUESTION_SKUS_REQUIRED)
        if survey_question_skus_required:
            survey_question_skus, secondary_survey_question_skus = \
                self.get_relevant_product_assortment_by_kpi_name(kpi_line[Const.KPI_NAME])
            unique_skus = \
                relevant_scif[relevant_scif['product_fk'].isin(
                    survey_question_skus)]['product_fk'].unique().tolist()
            if secondary_survey_question_skus:
                secondary_unique_skus = \
                    relevant_scif[relevant_scif['product_fk'].isin(secondary_survey_question_skus)][
                        'product_fk'].unique().tolist()
            else:
                secondary_unique_skus = None

        else:
            secondary_unique_skus = None
            manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
            if manufacturer:
                relevant_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]
            brand = self.does_exist(kpi_line, Const.BRAND)
            if brand:
                relevant_scif = relevant_scif[relevant_scif['brand_name'].isin(brand)]
            category = self.does_exist(kpi_line, Const.CATEGORY)
            if category:
                relevant_scif = relevant_scif[relevant_scif['category'].isin(category)]
            excluded_brand = self.does_exist(kpi_line, Const.EXCLUDED_BRAND)
            if excluded_brand:
                relevant_scif = relevant_scif[~relevant_scif['brand_name'].isin(excluded_brand)]
            excluded_sku = self.does_exist(kpi_line, Const.EXCLUDED_SKU)
            if excluded_sku:
                relevant_scif = relevant_scif[~relevant_scif['product_name'].isin(excluded_sku)]
            unique_skus = relevant_scif['product_fk'].unique().tolist()

        length_of_unique_skus = len(unique_skus)
        minimum_number_of_skus = kpi_line[Const.MINIMUM_NUMBER_OF_SKUS]

        if length_of_unique_skus >= minimum_number_of_skus:
            if secondary_unique_skus:
                length_of_unique_skus = len(secondary_unique_skus)
                minimum_number_of_skus = kpi_line[Const.SECONDARY_MINIMUM_NUMBER_OF_SKUS]
                result = 1 if length_of_unique_skus > minimum_number_of_skus else 0
            else:
                result = 1
        else:
            result = 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                          denominator_id=self.store_id, denominator_result=0, weight=weight,
                                          result=length_of_unique_skus, target=minimum_number_of_skus,
                                          score=result * weight,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    def get_relevant_product_assortment_by_kpi_name(self, kpi_name):
        template = self.templates[Const.SURVEY_QUESTION_SKUS]
        relevant_template = template[template[Const.KPI_NAME] == kpi_name]
        # we need this to fix dumb template
        relevant_template[Const.EAN_CODE] = \
            relevant_template[Const.EAN_CODE].apply(lambda x: str(int(x)) if x != '' else None)
        primary_ean_codes = \
            relevant_template[relevant_template[Const.SECONDARY_GROUP]
                              != 'Y'][Const.EAN_CODE].unique().tolist()
        primary_ean_codes = [code for code in primary_ean_codes if code is not None]
        primary_products = self.all_products[self.all_products['product_ean_code'].isin(
            primary_ean_codes)]
        primary_product_pks = primary_products['product_fk'].unique().tolist()
        secondary_ean_codes = \
            relevant_template[relevant_template[Const.SECONDARY_GROUP]
                              == 'Y'][Const.EAN_CODE].unique().tolist()
        if secondary_ean_codes:
            secondary_products = self.all_products[self.all_products['product_ean_code'].isin(
                secondary_ean_codes)]
            secondary_product_pks = secondary_products['product_fk'].unique().tolist()
        else:
            secondary_product_pks = None
        return primary_product_pks, secondary_product_pks

    # Count of Display functions
    def calculate_count_of_display(self, kpi_line, relevant_scif, weight):
        filtered_scif = relevant_scif.copy()

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = relevant_scif[relevant_scif['manufacturer_name'].isin(manufacturer)]

        liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
        if liberty_truck:
            liberty_truck_scif = relevant_scif[relevant_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                liberty_truck)]
            filtered_scif = filtered_scif.append(liberty_truck_scif, sort=False).drop_duplicates()

        brand = self.does_exist(kpi_line, Const.BRAND)
        if brand:
            filtered_scif = filtered_scif[filtered_scif['brand_name'].isin(brand)]

        category = self.does_exist(kpi_line, Const.CATEGORY)
        if category:
            filtered_scif = filtered_scif[filtered_scif['category'].isin(category)]

        excluded_brand = self.does_exist(kpi_line, Const.EXCLUDED_BRAND)
        if excluded_brand:
            filtered_scif = filtered_scif[~filtered_scif['brand_name'].isin(excluded_brand)]

        excluded_category = self.does_exist(kpi_line, Const.EXCLUDED_CATEGORY)
        if excluded_category:
            filtered_scif = filtered_scif[~filtered_scif['category'].isin(excluded_category)]

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            filtered_scif = filtered_scif[filtered_scif['att4'].isin(ssd_still)]

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        size_subpackages = self.does_exist(kpi_line, Const.SIZE_SUBPACKAGES_NUM)
        if size_subpackages:
            # convert all pairings of size and number of subpackages to tuples
            # size_subpackages_tuples = [tuple([float(i) for i in x.split(';')]) for x in size_subpackages]
            size_subpackages_tuples = [tuple([self.convert_base_size_values(i) for i in x.split(';')]) for x in
                                       size_subpackages]
            filtered_scif = filtered_scif[pd.Series(list(zip(filtered_scif['Base Size'],
                                                             filtered_scif['Multi-Pack Size'])),
                                                    index=filtered_scif.index).isin(size_subpackages_tuples)]

        excluded_size_subpackages = self.does_exist(kpi_line, Const.EXCLUDED_SIZE_SUBPACKAGES_NUM)
        if excluded_size_subpackages:
            # convert all pairings of size and number of subpackages to tuples
            # size_subpackages_tuples = [tuple([float(i) for i in x.split(';')]) for x in size_subpackages]
            size_subpackages_tuples = [tuple([self.convert_base_size_values(i) for i in x.split(';')]) for x in
                                       excluded_size_subpackages]
            filtered_scif = filtered_scif[~pd.Series(list(zip(filtered_scif['Base Size'],
                                                              filtered_scif['Multi-Pack Size'])),
                                                     index=filtered_scif.index).isin(size_subpackages_tuples)]

        sub_packages = self.does_exist(kpi_line, Const.SUBPACKAGES_NUM)
        if sub_packages:
            if sub_packages == [Const.NOT_NULL]:
                filtered_scif = filtered_scif[~filtered_scif['Multi-Pack Size'].isnull()]
            elif sub_packages == [Const.GREATER_THAN_ONE]:
                filtered_scif = filtered_scif[filtered_scif['Multi-Pack Size'] > 1]
            else:
                filtered_scif = filtered_scif[filtered_scif['Multi-Pack Size'].isin(
                    [int(i) for i in sub_packages])]

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            number_of_passing_displays, _ = self.get_number_of_passing_displays(filtered_scif)

            if self.does_exist(kpi_line, Const.PARENT_KPI_NAME):
                parent_kpi_name = kpi_line[Const.PARENT_KPI_NAME] + Const.LIBERTY + Const.DRILLDOWN
                kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(
                    kpi_line[Const.KPI_NAME] + Const.LIBERTY)
                self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                                  denominator_id=self.store_id, denominator_result=0, weight=weight,
                                                  result=number_of_passing_displays,
                                                  score=number_of_passing_displays,
                                                  identifier_parent=parent_kpi_name, should_enter=True)
                return 0
            else:
                parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
                identifier_result = parent_kpi_name + Const.DRILLDOWN
                kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
                self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk, numerator_result=0,
                                                  denominator_id=self.store_id, denominator_result=0, weight=weight,
                                                  result=number_of_passing_displays,
                                                  score=number_of_passing_displays * weight,
                                                  identifier_parent=parent_kpi_name,
                                                  identifier_result=identifier_result, should_enter=True)
                return number_of_passing_displays
        else:
            return 0

    # Share of Display functions
    def calculate_share_of_display(self, kpi_line, relevant_scif, weight):
        base_scif = relevant_scif.copy()

        ssd_still = self.does_exist(kpi_line, Const.ATT4)
        if ssd_still:
            ssd_still_scif = base_scif[base_scif['att4'].isin(ssd_still)]
        else:
            ssd_still_scif = base_scif

        denominator_passing_displays, _ = \
            self.get_number_of_passing_displays(ssd_still_scif)

        manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
        if manufacturer:
            filtered_scif = ssd_still_scif[ssd_still_scif['manufacturer_name'].isin(manufacturer)]
        else:
            filtered_scif = ssd_still_scif

        liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
        if liberty_truck:
            liberty_truck_scif = ssd_still_scif[ssd_still_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                liberty_truck)]
            filtered_scif = filtered_scif.append(liberty_truck_scif, sort=False).drop_duplicates()

        if self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET):
            market_share_target = self.get_market_share_target(ssd_still=ssd_still)
        else:
            market_share_target = 0

        if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
            body_armor_scif = relevant_scif[relevant_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
            filtered_scif = filtered_scif.append(body_armor_scif, sort=False)

        if self.does_exist(kpi_line, Const.MINIMUM_FACINGS_REQUIRED):
            numerator_passing_displays, _ = \
                self.get_number_of_passing_displays(filtered_scif)

            if denominator_passing_displays != 0:
                share_of_displays = \
                    numerator_passing_displays / float(denominator_passing_displays)
            else:
                share_of_displays = 0

            result = 1 if share_of_displays > market_share_target else 0

            parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
            kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
            self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk,
                                              numerator_result=numerator_passing_displays,
                                              denominator_id=self.store_id,
                                              denominator_result=denominator_passing_displays, weight=weight,
                                              result=share_of_displays * 100, target=market_share_target * 100,
                                              score=result * weight,
                                              identifier_parent=parent_kpi_name, should_enter=True)

            return result
        else:
            return 0

    def get_number_of_passing_displays(self, filtered_scif):
        if filtered_scif.empty:
            return 0, 0

        filtered_scif = \
            filtered_scif.groupby(['Base Size', 'Multi-Pack Size', 'scene_id'],
                                  as_index=False)['facings'].sum()

        filtered_scif['passed_displays'] = \
            filtered_scif.apply(lambda row: self._calculate_pass_status_of_display(row), axis=1)

        number_of_displays = filtered_scif['passed_displays'].sum()
        facings_of_displays = filtered_scif[filtered_scif['passed_displays'] == 1]['facings'].sum()

        return number_of_displays, facings_of_displays

    def _calculate_pass_status_of_display(self, row):  # need to move to external KPI targets
        template = self.templates[Const.MINIMUM_FACINGS]
        relevant_template = template[(template[Const.BASE_SIZE_MIN] <= row['Base Size']) &
                                     (template[Const.BASE_SIZE_MAX] >= row['Base Size']) &
                                     (template[Const.MULTI_PACK_SIZE] == row['Multi-Pack Size'])]
        if relevant_template.empty:
            return 0
        minimum_facings = relevant_template[Const.MINIMUM_FACINGS_REQUIRED_FOR_DISPLAY].min()
        return 1 if row['facings'] >= minimum_facings else 0

    # Share of Cooler functions
    def calculate_share_of_coolers(self, kpi_line, relevant_scif, weight):
        scene_ids = relevant_scif['scene_id'].unique().tolist()

        total_coolers = len(scene_ids)
        if total_coolers == 0:
            return 0

        passing_coolers = 0

        if self.does_exist(kpi_line, Const.MARKET_SHARE_TARGET):
            market_share_target = self.get_market_share_target()
        else:
            market_share_target = 0

        for scene_id in scene_ids:
            cooler_scif = relevant_scif[relevant_scif['scene_id'] == scene_id]

            filtered_scif = cooler_scif.copy()

            manufacturer = self.does_exist(kpi_line, Const.MANUFACTURER)
            if manufacturer:
                filtered_scif = cooler_scif[cooler_scif['manufacturer_name'].isin(manufacturer)]

            liberty_truck = self.does_exist(kpi_line, Const.LIBERTY_KEY_MANUFACTURER)
            if liberty_truck:
                liberty_truck_scif = cooler_scif[cooler_scif[Const.LIBERTY_KEY_MANUFACTURER].isin(
                    liberty_truck)]
                filtered_scif = filtered_scif.append(
                    liberty_truck_scif, sort=False).drop_duplicates()

            if self.does_exist(kpi_line, Const.INCLUDE_BODY_ARMOR) and self.body_armor_delivered:
                body_armor_scif = cooler_scif[cooler_scif['brand_fk'] == Const.BODY_ARMOR_BRAND_FK]
                filtered_scif = filtered_scif.append(body_armor_scif, sort=False).drop_duplicates()

            coke_facings_threshold = self.does_exist(kpi_line, Const.COKE_FACINGS_THRESHOLD)
            cooler_sos = filtered_scif['facings'].sum() / cooler_scif['facings'].sum()
            cooler_result = 1 if cooler_sos >= coke_facings_threshold else 0

            passing_coolers += cooler_result

        coke_market_share = passing_coolers / float(total_coolers)
        result = 1 if coke_market_share > market_share_target else 0

        parent_kpi_name = kpi_line[Const.KPI_NAME] + Const.LIBERTY
        kpi_fk = self.common_db.get_kpi_fk_by_kpi_type(parent_kpi_name + Const.DRILLDOWN)
        self.common_db.write_to_db_result(kpi_fk, numerator_id=self.manufacturer_fk,
                                          numerator_result=passing_coolers,
                                          denominator_id=self.store_id,
                                          denominator_result=total_coolers, weight=weight,
                                          result=coke_market_share * 100, target=market_share_target * 100,
                                          score=result * weight,
                                          identifier_parent=parent_kpi_name, should_enter=True)

        return result

    # Survey functions
    def calculate_survey(self, kpi_line, relevant_scif, weight):
        return 1 if self.survey.check_survey_answer(kpi_line[Const.QUESTION_TEXT], 'Yes') else 0

    # helper functions
    def convert_base_size_and_multi_pack(self):
        self.scif.loc[:, 'Base Size'] = self.scif['Base Size'].apply(self.convert_base_size_values)
        self.scif.loc[:, 'Multi-Pack Size'] = \
            self.scif['Multi-Pack Size'].apply(lambda x: int(x) if x is not None else None)

    @staticmethod
    def convert_base_size_values(value):
        try:
            new_value = float(value.split()[0]) if value not in [None, ''] else None
        except IndexError:
            Log.error('Could not convert base size value for {}'.format(value))
            new_value = None
        return new_value

    def get_market_share_target(self, ssd_still=None):  # need to move to external KPI targets
        template = self.templates[Const.MARKET_SHARE]
        relevant_template = template[(template[Const.ADDITIONAL_ATTRIBUTE_4] == self.additional_attribute_4) &
                                     (template[Const.RETAILER] == self.retailer) &
                                     (template[Const.BRANCH] == self.branch)]

        if relevant_template.empty:
            if ssd_still:
                if ssd_still[0].lower() == Const.SSD.lower():
                    return 49
                elif ssd_still[0].lower() == Const.STILL.lower():
                    return 16
                else:
                    return 0
            else:
                return 26

        if ssd_still:
            if ssd_still[0].lower() == Const.SSD.lower():
                return relevant_template[Const.SSD].iloc[0]
            elif ssd_still[0].lower() == Const.STILL.lower():
                return relevant_template[Const.STILL].iloc[0]

        # total 26, ssd only 49, still only 16
        return relevant_template[Const.SSD_AND_STILL].iloc[0]

    def get_body_armor_delivery_status(self):
        if self.store_info['additional_attribute_8'].iloc[0] == 'Y':
            return True
        else:
            return False

    def get_kpi_function(self, kpi_type):
        """
        transfers every kpi to its own function
        :param kpi_type: value from "sheet" column in the main sheet
        :return: function
        """
        if kpi_type == Const.SOS:
            return self.calculate_sos
        elif kpi_type == Const.AVAILABILITY:
            return self.calculate_availability
        elif kpi_type == Const.COUNT_OF_DISPLAY:
            return self.calculate_count_of_display
        elif kpi_type == Const.SHARE_OF_DISPLAY:
            return self.calculate_share_of_display
        elif kpi_type == Const.SHARE_OF_COOLERS:
            return self.calculate_share_of_coolers
        elif kpi_type == Const.SURVEY:
            return self.calculate_survey
        else:
            Log.warning(
                "The value '{}' in column sheet in the template is not recognized".format(kpi_type))
            return None

    @staticmethod
    def does_exist(kpi_line, column_name):
        """
        checks if kpi_line has values in this column, and if it does - returns a list of these values
        :param kpi_line: line from template
        :param column_name: str
        :return: list of values if there are, otherwise None
        """
        if column_name in kpi_line.keys() and kpi_line[column_name] != "":
            cell = kpi_line[column_name]
            if type(cell) in [int, float, np.float64]:
                return [cell]
            elif type(cell) in [unicode, str]:
                return [x.strip() for x in cell.split(",")]
        return None
예제 #3
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