示例#1
0
class CBCDAIRYILToolBox:
    def __init__(self, data_provider, output):
        self.output = output
        self.data_provider = data_provider
        self.project_name = self.data_provider.project_name
        self.common = Common(self.data_provider)
        self.old_common = oldCommon(self.data_provider)
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.session_fk = self.data_provider.session_id
        self.match_product_in_scene = self.data_provider[Data.MATCHES]
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.store_id = self.data_provider[Data.STORE_FK]
        self.survey = Survey(self.data_provider)
        self.block = Block(self.data_provider)
        self.general_toolbox = GENERALToolBox(self.data_provider)
        self.visit_date = self.data_provider[Data.VISIT_DATE]
        self.template_path = self.get_relevant_template()
        self.gap_data = self.get_gap_data()
        self.kpi_weights = parse_template(self.template_path,
                                          Consts.KPI_WEIGHT,
                                          lower_headers_row_index=0)
        self.template_data = self.parse_template_data()
        self.kpis_gaps = list()
        self.passed_availability = list()
        self.kpi_static_data = self.old_common.get_kpi_static_data()
        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]

    def get_relevant_template(self):
        """
        This function returns the relevant template according to it's visit date.
        Because of a change that was done in the logic there are 3 templates that match different dates.
        :return: Full template path
        """
        if self.visit_date <= datetime.date(datetime(2019, 12, 31)):
            return "{}/{}/{}".format(
                Consts.TEMPLATE_PATH, Consts.PREVIOUS_TEMPLATES,
                Consts.PROJECT_TEMPLATE_NAME_UNTIL_2019_12_31)
        else:
            return "{}/{}".format(Consts.TEMPLATE_PATH,
                                  Consts.CURRENT_TEMPLATE)

    def get_gap_data(self):
        """
        This function parse the gap data template and returns the gap priorities.
        :return: A dict with the priorities according to kpi_names. E.g: {kpi_name1: 1, kpi_name2: 2 ...}
        """
        gap_sheet = parse_template(self.template_path,
                                   Consts.KPI_GAP,
                                   lower_headers_row_index=0)
        gap_data = zip(gap_sheet[Consts.KPI_NAME], gap_sheet[Consts.ORDER])
        gap_data = {kpi_name: int(order) for kpi_name, order in gap_data}
        return gap_data

    def main_calculation(self):
        """
        This function calculates the KPI results.
        At first it fetches the relevant Sets (according to the stores attributes) and go over all of the relevant
        Atomic KPIs based on the project's template.
        Than, It aggregates the result per KPI using the weights and at last aggregates for the set level.
        """
        self.calculate_hierarchy_sos()
        self.calculate_oos()
        if self.template_data.empty:
            Log.warning(Consts.EMPTY_TEMPLATE_DATA_LOG.format(self.store_id))
            return
        kpi_set, kpis = self.get_relevant_kpis_for_calculation()
        kpi_set_fk = self.common.get_kpi_fk_by_kpi_type(Consts.TOTAL_SCORE)
        old_kpi_set_fk = self.get_kpi_fk_by_kpi_name(Consts.TOTAL_SCORE, 1)
        total_set_scores = list()
        for kpi_name in kpis:
            kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name)
            old_kpi_fk = self.get_kpi_fk_by_kpi_name(kpi_name, 2)
            kpi_weight = self.get_kpi_weight(kpi_name, kpi_set)
            atomics_df = self.get_atomics_to_calculate(kpi_name)
            atomic_results = self.calculate_atomic_results(
                kpi_fk, atomics_df)  # Atomic level
            kpi_results = self.calculate_kpis_and_save_to_db(
                atomic_results, kpi_fk, kpi_weight, kpi_set_fk)  # KPI lvl
            self.old_common.old_write_to_db_result(fk=old_kpi_fk,
                                                   level=2,
                                                   score=format(
                                                       kpi_results, '.2f'))
            total_set_scores.append(kpi_results)
        kpi_set_score = self.calculate_kpis_and_save_to_db(
            total_set_scores, kpi_set_fk)  # Set level
        self.old_common.write_to_db_result(fk=old_kpi_set_fk,
                                           level=1,
                                           score=kpi_set_score)
        self.handle_gaps()

    def calculate_oos(self):
        numerator = total_facings = 0
        store_kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_type=Consts.OOS)
        sku_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            kpi_type=Consts.OOS_SKU)
        leading_skus_df = self.template_data[self.template_data[
            Consts.KPI_NAME].str.encode(
                "utf8") == Consts.LEADING_PRODUCTS.encode("utf8")]
        skus_ean_list = leading_skus_df[Consts.PARAMS_VALUE_1].tolist()
        skus_ean_set = set([
            ean_code.strip() for values in skus_ean_list
            for ean_code in values.split(",")
        ])
        product_fks = self.all_products[self.all_products[
            'product_ean_code'].isin(skus_ean_set)]['product_fk'].tolist()
        # sku level oos
        for sku in product_fks:
            # 2 for distributed and 1 for oos
            product_df = self.scif[self.scif['product_fk'] == sku]
            if product_df.empty:
                numerator += 1
                self.common.write_to_db_result(fk=sku_kpi_fk,
                                               numerator_id=sku,
                                               denominator_id=self.store_id,
                                               result=1,
                                               numerator_result=1,
                                               denominator_result=1,
                                               score=0,
                                               identifier_parent="OOS",
                                               should_enter=True)

        # store level oos
        denominator = len(product_fks)
        if denominator == 0:
            numerator = result = 0
        else:
            result = round(numerator / float(denominator), 4)
        self.common.write_to_db_result(fk=store_kpi_fk,
                                       numerator_id=self.own_manufacturer_fk,
                                       denominator_id=self.store_id,
                                       result=result,
                                       numerator_result=numerator,
                                       denominator_result=denominator,
                                       score=total_facings,
                                       identifier_result="OOS")

    def calculate_hierarchy_sos(self):
        store_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            kpi_type=Consts.SOS_BY_OWN_MAN)
        category_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            kpi_type=Consts.SOS_BY_OWN_MAN_CAT)
        brand_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            kpi_type=Consts.SOS_BY_OWN_MAN_CAT_BRAND)
        sku_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            kpi_type=Consts.SOS_BY_OWN_MAN_CAT_BRAND_SKU)
        sos_df = self.scif[self.scif['rlv_sos_sc'] == 1]
        # store level sos
        store_res, store_num, store_den = self.calculate_own_manufacturer_sos(
            filters={}, df=sos_df)
        self.common.write_to_db_result(fk=store_kpi_fk,
                                       numerator_id=self.own_manufacturer_fk,
                                       denominator_id=self.store_id,
                                       result=store_res,
                                       numerator_result=store_num,
                                       denominator_result=store_den,
                                       score=store_res,
                                       identifier_result="OWN_SOS")
        # category level sos
        session_categories = set(
            self.parser.filter_df(
                conditions={'manufacturer_fk': self.own_manufacturer_fk},
                data_frame_to_filter=self.scif)['category_fk'])
        for category_fk in session_categories:
            filters = {'category_fk': category_fk}
            cat_res, cat_num, cat_den = self.calculate_own_manufacturer_sos(
                filters=filters, df=sos_df)
            self.common.write_to_db_result(
                fk=category_kpi_fk,
                numerator_id=category_fk,
                denominator_id=self.store_id,
                result=cat_res,
                numerator_result=cat_num,
                denominator_result=cat_den,
                score=cat_res,
                identifier_parent="OWN_SOS",
                should_enter=True,
                identifier_result="OWN_SOS_cat_{}".format(str(category_fk)))
            # brand-category level sos
            filters['manufacturer_fk'] = self.own_manufacturer_fk
            cat_brands = set(
                self.parser.filter_df(conditions=filters,
                                      data_frame_to_filter=sos_df)['brand_fk'])
            for brand_fk in cat_brands:
                filters['brand_fk'] = brand_fk
                brand_df = self.parser.filter_df(conditions=filters,
                                                 data_frame_to_filter=sos_df)
                brand_num = brand_df['facings'].sum()
                brand_res, brand_num, cat_num = self.calculate_sos_res(
                    brand_num, cat_num)
                self.common.write_to_db_result(
                    fk=brand_kpi_fk,
                    numerator_id=brand_fk,
                    denominator_id=category_fk,
                    result=brand_res,
                    numerator_result=brand_num,
                    should_enter=True,
                    denominator_result=cat_num,
                    score=brand_res,
                    identifier_parent="OWN_SOS_cat_{}".format(
                        str(category_fk)),
                    identifier_result="OWN_SOS_cat_{}_brand_{}".format(
                        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_facings = product_df['facings'].sum()
                    sku_result, sku_num, sku_den = self.calculate_sos_res(
                        sku_facings, brand_num)
                    self.common.write_to_db_result(
                        fk=sku_kpi_fk,
                        numerator_id=sku,
                        denominator_id=brand_fk,
                        result=sku_result,
                        numerator_result=sku_facings,
                        should_enter=True,
                        denominator_result=brand_num,
                        score=sku_facings,
                        identifier_parent="OWN_SOS_cat_{}_brand_{}".format(
                            str(category_fk), str(brand_fk)))
                del filters['product_fk']
            del filters['brand_fk']

    def calculate_own_manufacturer_sos(self, filters, df):
        filters['manufacturer_fk'] = self.own_manufacturer_fk
        numerator_df = self.parser.filter_df(conditions=filters,
                                             data_frame_to_filter=df)
        del filters['manufacturer_fk']
        denominator_df = self.parser.filter_df(conditions=filters,
                                               data_frame_to_filter=df)
        if denominator_df.empty:
            return 0, 0, 0
        denominator = denominator_df['facings'].sum()
        if numerator_df.empty:
            numerator = 0
        else:
            numerator = numerator_df['facings'].sum()
        return self.calculate_sos_res(numerator, denominator)

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

    def add_gap(self, atomic_kpi, score, atomic_weight):
        """
        In case the score is not perfect the gap is added to the gap list.
        :param atomic_weight: The Atomic KPI's weight.
        :param score: Atomic KPI score.
        :param atomic_kpi: A Series with data about the Atomic KPI.
        """
        parent_kpi_name = atomic_kpi[Consts.KPI_NAME]
        atomic_name = atomic_kpi[Consts.KPI_ATOMIC_NAME]
        atomic_fk = self.common.get_kpi_fk_by_kpi_type(atomic_name)
        current_gap_dict = {
            Consts.ATOMIC_FK: atomic_fk,
            Consts.PRIORITY: self.gap_data[parent_kpi_name],
            Consts.SCORE: score,
            Consts.WEIGHT: atomic_weight
        }
        self.kpis_gaps.append(current_gap_dict)

    @staticmethod
    def sort_by_priority(gap_dict):
        """ This is a util function for the kpi's gaps sorting by priorities"""
        return gap_dict[Consts.PRIORITY], gap_dict[Consts.SCORE]

    def handle_gaps(self):
        """ This function takes the top 5 gaps (by priority) and saves it to the DB (pservice.custom_gaps table) """
        self.kpis_gaps.sort(key=self.sort_by_priority)
        gaps_total_score = 0
        gaps_per_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.GAP_PER_ATOMIC_KPI)
        gaps_total_score_kpi_fk = self.common.get_kpi_fk_by_kpi_type(
            Consts.GAPS_TOTAL_SCORE_KPI)
        for gap in self.kpis_gaps[:5]:
            current_gap_score = gap[Consts.WEIGHT] - (gap[Consts.SCORE] / 100 *
                                                      gap[Consts.WEIGHT])
            gaps_total_score += current_gap_score
            self.insert_gap_results(gaps_per_kpi_fk,
                                    current_gap_score,
                                    gap[Consts.WEIGHT],
                                    numerator_id=gap[Consts.ATOMIC_FK],
                                    parent_fk=gaps_total_score_kpi_fk)
        total_weight = sum(
            map(lambda res: res[Consts.WEIGHT], self.kpis_gaps[:5]))
        self.insert_gap_results(gaps_total_score_kpi_fk, gaps_total_score,
                                total_weight)

    def insert_gap_results(self,
                           gap_kpi_fk,
                           score,
                           weight,
                           numerator_id=Consts.CBC_MANU,
                           parent_fk=None):
        """ This is a utility function that insert results to the DB for the GAP """
        should_enter = True if parent_fk else False
        score, weight = score * 100, round(weight * 100, 2)
        self.common.write_to_db_result(fk=gap_kpi_fk,
                                       numerator_id=numerator_id,
                                       numerator_result=score,
                                       denominator_id=self.store_id,
                                       denominator_result=weight,
                                       weight=weight,
                                       identifier_result=gap_kpi_fk,
                                       identifier_parent=parent_fk,
                                       result=score,
                                       score=score,
                                       should_enter=should_enter)

    def calculate_kpis_and_save_to_db(self,
                                      kpi_results,
                                      kpi_fk,
                                      parent_kpi_weight=1.0,
                                      parent_fk=None):
        """
        This KPI aggregates the score by weights and saves the results to the DB.
        :param kpi_results: A list of results and weights tuples: [(score1, weight1), (score2, weight2) ... ].
        :param kpi_fk: The relevant KPI fk.
        :param parent_kpi_weight: The parent's KPI total weight.
        :param parent_fk: The KPI SET FK that the KPI "belongs" too if exist.
        :return: The aggregated KPI score.
        """
        should_enter = True if parent_fk else False
        ignore_weight = not should_enter  # Weights should be ignored only in the set level!
        kpi_score = self.calculate_kpi_result_by_weight(
            kpi_results, parent_kpi_weight, ignore_weights=ignore_weight)
        total_weight = round(parent_kpi_weight * 100, 2)
        target = None if parent_fk else round(80,
                                              2)  # Requested for visualization
        self.common.write_to_db_result(fk=kpi_fk,
                                       numerator_id=Consts.CBC_MANU,
                                       numerator_result=kpi_score,
                                       denominator_id=self.store_id,
                                       denominator_result=total_weight,
                                       target=target,
                                       identifier_result=kpi_fk,
                                       identifier_parent=parent_fk,
                                       should_enter=should_enter,
                                       weight=total_weight,
                                       result=kpi_score,
                                       score=kpi_score)

        if not parent_fk:  # required only for writing set score in anoter kpi needed for dashboard
            kpi_fk = self.common.get_kpi_fk_by_kpi_type(
                Consts.TOTAL_SCORE_FOR_DASHBOARD)
            self.common.write_to_db_result(fk=kpi_fk,
                                           numerator_id=Consts.CBC_MANU,
                                           numerator_result=kpi_score,
                                           denominator_id=self.store_id,
                                           denominator_result=total_weight,
                                           target=target,
                                           identifier_result=kpi_fk,
                                           identifier_parent=parent_fk,
                                           should_enter=should_enter,
                                           weight=total_weight,
                                           result=kpi_score,
                                           score=kpi_score)

        return kpi_score

    def calculate_kpi_result_by_weight(self,
                                       kpi_results,
                                       parent_kpi_weight,
                                       ignore_weights=False):
        """
        This function aggregates the KPI results by scores and weights.
        :param ignore_weights: If True the function just sums the results.
        :param parent_kpi_weight: The parent's KPI total weight.
        :param kpi_results: A list of results and weights tuples: [(score1, weight1), (score2, weight2) ... ].
        :return: The aggregated KPI score.
        """
        if ignore_weights or len(kpi_results) == 0:
            return sum(kpi_results)
        weights_list = map(lambda res: res[1], kpi_results)
        if None in weights_list:  # Ignoring weights and dividing equally by length!
            kpi_score = sum(map(lambda res: res[0], kpi_results)) / float(
                len(kpi_results))
        elif round(
                sum(weights_list), 2
        ) < parent_kpi_weight:  # Missing weights needs to be divided among the kpis
            kpi_score = self.divide_missing_percentage(kpi_results,
                                                       parent_kpi_weight,
                                                       sum(weights_list))
        else:
            kpi_score = sum([score * weight for score, weight in kpi_results])
        return kpi_score

    @staticmethod
    def divide_missing_percentage(kpi_results, parent_weight, total_weights):
        """
        This function is been activated in case the total number of KPI weights doesn't equal to 100%.
        It divides the missing percentage among the other KPI and calculates the score.
        :param parent_weight: Parent KPI's weight.
        :param total_weights: The total number of weights that were calculated earlier.
        :param kpi_results: A list of results and weights tuples: [(score1, weight1), (score2, weight2) ... ].
        :return: KPI aggregated score.
        """
        missing_weight = parent_weight - total_weights
        weight_addition = missing_weight / float(
            len(kpi_results)) if kpi_results else 0
        kpi_score = sum([
            score * (weight + weight_addition) for score, weight in kpi_results
        ])
        return kpi_score

    def calculate_atomic_results(self, kpi_fk, atomics_df):
        """
        This method calculates the result for every atomic KPI (the lowest level) that are relevant for the kpi_fk.
        :param kpi_fk: The KPI FK that the atomic "belongs" too.
        :param atomics_df: The relevant Atomic KPIs from the project's template.
        :return: A list of results and weights tuples: [(score1, weight1), (score2, weight2) ... ].
        """
        total_scores = list()
        for i in atomics_df.index:
            current_atomic = atomics_df.loc[i]
            kpi_type, atomic_weight, general_filters = self.get_relevant_data_per_atomic(
                current_atomic)
            if general_filters is None:
                continue
            num_result, den_result, atomic_score = self.calculate_atomic_kpi_by_type(
                kpi_type, **general_filters)
            # Handling Atomic KPIs results
            if atomic_score is None:  # In cases that we need to ignore the KPI and divide it's weight
                continue
            elif atomic_score < 100:
                self.add_gap(current_atomic, atomic_score, atomic_weight)
            total_scores.append((atomic_score, atomic_weight))
            atomic_fk_lvl_2 = self.common.get_kpi_fk_by_kpi_type(
                current_atomic[Consts.KPI_ATOMIC_NAME].strip())
            old_atomic_fk = self.get_kpi_fk_by_kpi_name(
                current_atomic[Consts.KPI_ATOMIC_NAME].strip(), 3)
            self.common.write_to_db_result(fk=atomic_fk_lvl_2,
                                           numerator_id=Consts.CBC_MANU,
                                           numerator_result=num_result,
                                           denominator_id=self.store_id,
                                           weight=round(
                                               atomic_weight * 100, 2),
                                           denominator_result=den_result,
                                           should_enter=True,
                                           identifier_parent=kpi_fk,
                                           result=atomic_score,
                                           score=atomic_score * atomic_weight)
            self.old_common.old_write_to_db_result(
                fk=old_atomic_fk,
                level=3,
                result=str(format(atomic_score * atomic_weight, '.2f')),
                score=atomic_score)
        return total_scores

    def get_kpi_fk_by_kpi_name(self, kpi_name, kpi_level):
        if kpi_level == 1:
            column_key = 'kpi_set_fk'
            column_value = 'kpi_set_name'
        elif kpi_level == 2:
            column_key = 'kpi_fk'
            column_value = 'kpi_name'
        elif kpi_level == 3:
            column_key = 'atomic_kpi_fk'
            column_value = 'atomic_kpi_name'
        else:
            raise ValueError('invalid level')

        try:
            if column_key and column_value:
                return self.kpi_static_data[
                    self.kpi_static_data[column_value].str.encode('utf-8') ==
                    kpi_name.encode('utf-8')][column_key].values[0]

        except IndexError:
            Log.error(
                'Kpi name: {}, isnt equal to any kpi name in static table'.
                format(kpi_name))
            return None

    def get_relevant_data_per_atomic(self, atomic_series):
        """
        This function return the relevant data per Atomic KPI.
        :param atomic_series: The Atomic row from the Template.
        :return: A tuple with data: (atomic_type, atomic_weight, general_filters)
        """
        kpi_type = atomic_series.get(Consts.KPI_TYPE)
        atomic_weight = float(atomic_series.get(
            Consts.WEIGHT)) if atomic_series.get(Consts.WEIGHT) else None
        general_filters = self.get_general_filters(atomic_series)
        return kpi_type, atomic_weight, general_filters

    def calculate_atomic_kpi_by_type(self, atomic_type, **general_filters):
        """
        This function calculates the result according to the relevant Atomic Type.
        :param atomic_type: KPI Family from the template.
        :param general_filters: Relevant attributes and values to calculate by.
        :return: A tuple with results: (numerator_result, denominator_result, total_score).
        """
        num_result = denominator_result = 0
        if atomic_type in [Consts.AVAILABILITY]:
            atomic_score = self.calculate_availability(**general_filters)
        elif atomic_type == Consts.AVAILABILITY_FROM_BOTTOM:
            atomic_score = self.calculate_availability_from_bottom(
                **general_filters)
        elif atomic_type == Consts.MIN_2_AVAILABILITY:
            num_result, denominator_result, atomic_score = self.calculate_min_2_availability(
                **general_filters)
        elif atomic_type == Consts.SURVEY:
            atomic_score = self.calculate_survey(**general_filters)
        elif atomic_type == Consts.BRAND_BLOCK:
            atomic_score = self.calculate_brand_block(**general_filters)
        elif atomic_type == Consts.EYE_LEVEL:
            num_result, denominator_result, atomic_score = self.calculate_eye_level(
                **general_filters)
        else:
            Log.warning(Consts.UNSUPPORTED_KPI_LOG.format(atomic_type))
            atomic_score = None
        return num_result, denominator_result, atomic_score

    def get_relevant_kpis_for_calculation(self):
        """
        This function retrieve the relevant KPIs to calculate from the template
        :return: A tuple: (set_name, [kpi1, kpi2, kpi3...]) to calculate.
        """
        kpi_set = self.template_data[Consts.KPI_SET].values[0]
        kpis = self.template_data[self.template_data[
            Consts.KPI_SET].str.encode('utf-8') == kpi_set.encode('utf-8')][
                Consts.KPI_NAME].unique().tolist()
        # Planogram KPI should be calculated last because of the MINIMUM 2 FACINGS KPI.
        if Consts.PLANOGRAM_KPI in kpis and kpis.index(
                Consts.PLANOGRAM_KPI) != len(kpis) - 1:
            kpis.append(kpis.pop(kpis.index(Consts.PLANOGRAM_KPI)))
        return kpi_set, kpis

    def get_atomics_to_calculate(self, kpi_name):
        """
        This method filters the KPIs data to be the relevant atomic KPIs.
        :param kpi_name: The hebrew KPI name from the template.
        :return: A DataFrame that contains data about the relevant Atomic KPIs.
        """
        atomics = self.template_data[self.template_data[
            Consts.KPI_NAME].str.encode('utf-8') == kpi_name.encode('utf-8')]
        return atomics

    def get_store_attributes(self, attributes_names):
        """
        This function encodes and returns the relevant store attribute.
        :param attributes_names: List of requested store attributes to return.
        :return: A dictionary with the requested attributes, E.g: {attr_name: attr_val, ...}
        """
        # Filter store attributes
        store_info_dict = self.store_info.iloc[0].to_dict()
        filtered_store_info = {
            store_att: store_info_dict[store_att]
            for store_att in attributes_names
        }
        return filtered_store_info

    def parse_template_data(self):
        """
        This function responsible to filter the relevant template data..
        :return: A DataFrame with filtered Data by store attributes.
        """
        kpis_template = parse_template(self.template_path,
                                       Consts.KPI_SHEET,
                                       lower_headers_row_index=1)
        relevant_store_info = self.get_store_attributes(
            Consts.STORE_ATTRIBUTES_TO_FILTER_BY)
        filtered_data = self.filter_template_by_store_att(
            kpis_template, relevant_store_info)
        return filtered_data

    @staticmethod
    def filter_template_by_store_att(kpis_template, store_attributes):
        """
        This function gets a dictionary with store type, additional attribute 1, 2 and 3 and filters the template by it.
        :param kpis_template: KPI sheet of the project's template.
        :param store_attributes: {store_type: X, additional_attribute_1: Y, ... }.
        :return: A filtered DataFrame.
        """
        for store_att, store_val in store_attributes.iteritems():
            if store_val is None:
                store_val = ""
            kpis_template = kpis_template[(
                kpis_template[store_att].str.encode('utf-8') ==
                store_val.encode('utf-8')) | (kpis_template[store_att] == "")]
        return kpis_template

    def get_relevant_scenes_by_params(self, params):
        """
        This function returns the relevant scene_fks to calculate.
        :param params: The Atomic KPI row filters from the template.
        :return: List of scene fks.
        """
        template_names = params[Consts.TEMPLATE_NAME].split(Consts.SEPARATOR)
        template_groups = params[Consts.TEMPLATE_GROUP].split(Consts.SEPARATOR)
        filtered_scif = self.scif[[
            Consts.SCENE_ID, 'template_name', 'template_group'
        ]]
        if template_names and any(template_names):
            filtered_scif = filtered_scif[filtered_scif['template_name'].isin(
                template_names)]
        if template_groups and any(template_groups):
            filtered_scif = filtered_scif[filtered_scif['template_group'].isin(
                template_groups)]
        return filtered_scif[Consts.SCENE_ID].unique().tolist()

    def get_general_filters(self, params):
        """
        This function returns the relevant KPI filters according to the template.
        Filter params 1 & 2 are included and param 3 is for exclusion.
        :param params: The Atomic KPI row in the template
        :return: A dictionary with the relevant filters.
        """
        general_filters = {
            Consts.TARGET: params[Consts.TARGET],
            Consts.SPLIT_SCORE: params[Consts.SPLIT_SCORE],
            Consts.KPI_FILTERS: dict()
        }
        relevant_scenes = self.get_relevant_scenes_by_params(params)
        if not relevant_scenes:
            return None
        else:
            general_filters[Consts.KPI_FILTERS][
                Consts.SCENE_ID] = relevant_scenes
        for type_col, value_col in Consts.KPI_FILTER_VALUE_LIST:
            if params[value_col]:
                should_included = Consts.INCLUDE_VAL if value_col != Consts.PARAMS_VALUE_3 else Consts.EXCLUDE_VAL
                param_type, param_value = params[type_col], params[value_col]
                filter_param = self.handle_param_values(
                    param_type, param_value)
                general_filters[Consts.KPI_FILTERS][param_type] = (
                    filter_param, should_included)

        return general_filters

    @staticmethod
    def handle_param_values(param_type, param_value):
        """
        :param param_type: The param type to filter by. E.g: product_ean code or brand_name
        :param param_value: The value to filter by.
        :return: list of param values.
        """
        values_list = param_value.split(Consts.SEPARATOR)
        params = map(
            lambda val: float(val) if unicode.isdigit(val) and param_type !=
            Consts.EAN_CODE else val.strip(), values_list)
        return params

    def get_kpi_weight(self, kpi, kpi_set):
        """
        This method returns the KPI weight according to the project's template.
        :param kpi: The KPI name.
        :param kpi_set: Set KPI name.
        :return: The kpi weight (Float).
        """
        row = self.kpi_weights[(self.kpi_weights[Consts.KPI_SET].str.encode(
            'utf-8') == kpi_set.encode('utf-8')) & (self.kpi_weights[
                Consts.KPI_NAME].str.encode('utf-8') == kpi.encode('utf-8'))]
        weight = row.get(Consts.WEIGHT)
        return float(weight.values[0]) if not weight.empty else None

    def merge_and_filter_scif_and_matches_for_eye_level(self, **kpi_filters):
        """
        This function merges between scene_item_facts and match_product_in_scene DataFrames and filters the merged DF
        according to the @param kpi_filters.
        :param kpi_filters: Dictionary with attributes and values to filter the DataFrame by.
        :return: The merged and filtered DataFrame.
        """
        scif_matches_diff = self.match_product_in_scene[
            ['scene_fk', 'product_fk'] +
            list(self.match_product_in_scene.keys().difference(
                self.scif.keys()))]
        merged_df = pd.merge(self.scif[self.scif.facings != 0],
                             scif_matches_diff,
                             how='outer',
                             left_on=['scene_id', 'item_id'],
                             right_on=[Consts.SCENE_FK, Consts.PRODUCT_FK])
        merged_df = merged_df[self.general_toolbox.get_filter_condition(
            merged_df, **kpi_filters)]
        return merged_df

    @kpi_runtime()
    def calculate_eye_level(self, **general_filters):
        """
        This function calculates the Eye level KPI. It filters and products according to the template and
        returns a Tuple: (eye_level_facings / total_facings, score).
        :param general_filters: A dictionary with the relevant KPI filters.
        :return: E.g: (10, 20, 50) or (8, 10, 100) --> score >= 75 turns to 100.
        """
        merged_df = self.merge_and_filter_scif_and_matches_for_eye_level(
            **general_filters[Consts.KPI_FILTERS])
        relevant_scenes = merged_df['scene_id'].unique().tolist()
        total_number_of_facings = eye_level_facings = 0
        for scene in relevant_scenes:
            scene_merged_df = merged_df[merged_df['scene_id'] == scene]
            scene_matches = self.match_product_in_scene[
                self.match_product_in_scene['scene_fk'] == scene]
            total_number_of_facings += len(scene_merged_df)
            scene_merged_df = self.filter_df_by_shelves(
                scene_merged_df, scene_matches, Consts.EYE_LEVEL_PER_SHELF)
            eye_level_facings += len(scene_merged_df)
        total_score = eye_level_facings / float(
            total_number_of_facings) if total_number_of_facings else 0
        total_score = 100 if total_score >= 0.75 else total_score * 100
        return eye_level_facings, total_number_of_facings, total_score

    @staticmethod
    def filter_df_by_shelves(df, scene_matches, eye_level_definition):
        """
        This function filters the df according to the eye-level definition
        :param df: data frame to filter
        :param scene_matches: match_product_in_scene for particular scene
        :param eye_level_definition: definition for eye level shelves
        :return: filtered data frame
        """
        # number_of_shelves = df.shelf_number_from_bottom.max()
        number_of_shelves = max(scene_matches.shelf_number_from_bottom.max(),
                                scene_matches.shelf_number.max())
        top, bottom = 0, 0
        for json_def in eye_level_definition:
            if json_def[Consts.MIN] <= number_of_shelves <= json_def[
                    Consts.MAX]:
                top = json_def[Consts.TOP]
                bottom = json_def[Consts.BOTTOM]
        return df[(df.shelf_number > top)
                  & (df.shelf_number_from_bottom > bottom)]

    @kpi_runtime()
    def calculate_availability_from_bottom(self, **general_filters):
        """
        This function checks if *all* of the relevant products are in the lowest shelf.
        :param general_filters: A dictionary with the relevant KPI filters.
        :return:
        """
        allowed_products_dict = self.get_allowed_product_by_params(
            **general_filters)
        filtered_matches = self.match_product_in_scene[
            self.match_product_in_scene[Consts.PRODUCT_FK].isin(
                allowed_products_dict[Consts.PRODUCT_FK])]
        relevant_shelves_to_check = set(
            filtered_matches[Consts.SHELF_NUM_FROM_BOTTOM].unique().tolist())
        # Check bottom shelf condition
        return 0 if len(
            relevant_shelves_to_check
        ) != 1 or Consts.LOWEST_SHELF not in relevant_shelves_to_check else 100

    @kpi_runtime()
    def calculate_brand_block(self, **general_filters):
        """
        This function calculates the brand block KPI. It filters and excluded products according to the template and
        than checks if at least one scene has a block.
        :param general_filters: A dictionary with the relevant KPI filters.
        :return: 100 if at least one scene has a block, 0 otherwise.
        """
        products_dict = self.get_allowed_product_by_params(**general_filters)
        block_result = self.block.network_x_block_together(
            population=products_dict,
            additional={
                'minimum_block_ratio': Consts.MIN_BLOCK_RATIO,
                'minimum_facing_for_block': Consts.MIN_FACINGS_IN_BLOCK,
                'allowed_products_filters': {
                    'product_type': ['Empty']
                },
                'calculate_all_scenes': False,
                'include_stacking': True,
                'check_vertical_horizontal': False
            })

        result = 100 if not block_result.empty and not block_result[
            block_result.is_block].empty else 0
        return result

    def get_allowed_product_by_params(self, **filters):
        """
        This function filters the relevant products for the block together KPI and exclude the ones that needs to be
        excluded by the template.
        :param filters: Atomic KPI filters.
        :return: A Dictionary with the relevant products. E.g: {'product_fk': [1,2,3,4,5]}.
        """
        allowed_product = dict()
        filtered_scif = self.calculate_availability(return_df=True, **filters)
        allowed_product[Consts.PRODUCT_FK] = filtered_scif[
            Consts.PRODUCT_FK].unique().tolist()
        return allowed_product

    @kpi_runtime()
    def calculate_survey(self, **general_filters):
        """
        This function calculates the result for Survey KPI.
        :param general_filters: A dictionary with the relevant KPI filters.
        :return: 100 if the answer is yes, else 0.
        """
        if Consts.QUESTION_ID not in general_filters[
                Consts.KPI_FILTERS].keys():
            Log.warning(Consts.MISSING_QUESTION_LOG)
            return 0
        survey_question_id = general_filters[Consts.KPI_FILTERS].get(
            Consts.QUESTION_ID)
        # General filters returns output for filter_df basically so we need to adjust it here.
        if isinstance(survey_question_id, tuple):
            survey_question_id = survey_question_id[0]  # Get rid of the tuple
        if isinstance(survey_question_id, list):
            survey_question_id = int(
                survey_question_id[0])  # Get rid of the list
        target_answer = general_filters[Consts.TARGET]
        survey_answer = self.survey.get_survey_answer(
            (Consts.QUESTION_FK, survey_question_id))
        if survey_answer in Consts.SURVEY_ANSWERS_TO_IGNORE:
            return None
        elif survey_answer:
            return 100 if survey_answer.strip() == target_answer else 0
        return 0

    @kpi_runtime()
    def calculate_availability(self, return_df=False, **general_filters):
        """
        This functions checks for availability by filters.
        During the calculation, if the KPI was passed, the results is being saved for future usage of
        "MIN 2 AVAILABILITY KPI".
        :param return_df: If True, the function returns the filtered scene item facts, else, returns the score.
        :param general_filters: A dictionary with the relevant KPI filters.
        :return: See @param return_df.
        """
        filtered_scif = self.scif[self.general_toolbox.get_filter_condition(
            self.scif, **general_filters[Consts.KPI_FILTERS])]
        if return_df:
            return filtered_scif
        if not filtered_scif.empty:
            tested_products = general_filters[Consts.KPI_FILTERS][
                Consts.EAN_CODE][0]
            self.passed_availability.append(tested_products)
            return 100
        return 0

    @staticmethod
    def get_number_of_facings_per_product_dict(df, ignore_stack=False):
        """
        This function gets a DataFrame and returns a dictionary with number of facings per products.
        :param df: Pandas.DataFrame with 'product_ean_code' and 'facings' / 'facings_ign_stack' fields.
        :param ignore_stack: If True will use 'facings_ign_stack' field, else 'facings' field.
        :return: E.g: {ean_code1: 10, ean_code2: 5, ean_code3: 1...}
        """
        stacking_field = Consts.FACINGS_IGN_STACK if ignore_stack else Consts.FACINGS
        df = df[[Consts.EAN_CODE, stacking_field]].dropna()
        df = df[df[stacking_field] > 0]
        facings_dict = dict(zip(df[Consts.EAN_CODE], df[stacking_field]))
        return facings_dict

    @kpi_runtime()
    def calculate_min_2_availability(self, **general_filters):
        """
        This KPI checks for all of the Availability Atomics KPIs that passed, if the tested products have at least
        2 facings in case of IGNORE STACKING!
        :param general_filters: A dictionary with the relevant KPI filters.
        :return: numerator result, denominator result and total_score
        """
        score = 0
        filtered_df = self.calculate_availability(return_df=True,
                                                  **general_filters)
        facings_counter = self.get_number_of_facings_per_product_dict(
            filtered_df, ignore_stack=True)
        for products in self.passed_availability:
            score += 1 if sum([
                facings_counter[product]
                for product in products if product in facings_counter
            ]) > 1 else 0
        total_score = (score / float(len(self.passed_availability))
                       ) * 100 if self.passed_availability else 0
        return score, len(self.passed_availability), total_score
示例#2
0
class INBEVMXToolBox:
    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.session_id = self.data_provider.session_id
        self.products = self.data_provider[Data.PRODUCTS]
        self.common_v2 = Common_V2(self.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.tools = GENERALToolBox(self.data_provider)
        self.scif = self.data_provider[Data.SCENE_ITEM_FACTS]
        self.survey = Survey(self.data_provider, self.output)
        self.rds_conn = PSProjectConnector(self.project_name,
                                           DbUsers.CalculationEng)
        self.kpi_static_data = self.common_v2.get_kpi_static_data()
        self.kpi_results_queries = []
        self.kpi_results_new_tables_queries = []
        self.store_info = self.data_provider[Data.STORE_INFO]
        self.oos_policies = self.get_policies()
        self.result_dict = {}
        self.hierarchy_dict = {}

        try:
            self.store_type_filter = self.store_info['store_type'].values[
                0].strip()
        except:
            Log.error("there is no store type in the db")
            return
        try:
            self.region_name_filter = self.store_info['region_name'].values[
                0].strip()
            self.region_fk = self.store_info['region_fk'].values[0]
        except:
            Log.error("there is no region in the db")
            return
        try:
            self.att6_filter = self.store_info[
                'additional_attribute_6'].values[0].strip()
        except:
            Log.error("there is no additional attribute 6 in the db")
            return
        self.sos_target_sheet = pd.read_excel(PATH_SURVEY_AND_SOS_TARGET,
                                              Const.SOS_TARGET).fillna("")
        self.survey_sheet = pd.read_excel(PATH_SURVEY_AND_SOS_TARGET,
                                          Const.SURVEY).fillna("")
        self.survey_combo_sheet = pd.read_excel(PATH_SURVEY_AND_SOS_TARGET,
                                                Const.SURVEY_COMBO).fillna("")
        self.oos_sheet = pd.read_excel(PATH_SURVEY_AND_SOS_TARGET,
                                       Const.OOS_KPI).fillna("")

    def get_policies(self):
        query = INBEVMXQueries.get_policies()
        policies = pd.read_sql_query(query, self.rds_conn.db)
        return policies

    def main_calculation(self):
        """
        This function calculates the KPI results.
        """
        kpis_sheet = pd.read_excel(PATH_SURVEY_AND_SOS_TARGET,
                                   Const.KPIS).fillna("")
        for index, row in kpis_sheet.iterrows():
            self.handle_atomic(row)
        self.save_parent_kpis()
        self.common_v2.commit_results_data()

    def calculate_oos_target(self):
        temp = self.oos_sheet[Const.TEMPLATE_STORE_TYPE]
        rows_stores_filter = self.oos_sheet[
            temp.apply(lambda r: self.store_type_filter in
                       [item.strip() for item in r.split(",")])]
        if rows_stores_filter.empty:
            weight = 0
        else:
            weight = rows_stores_filter[Const.TEMPLATE_SCORE].values[0]
        all_data = pd.merge(
            self.scif[["store_id", "product_fk", "facings", "template_name"]],
            self.store_info,
            left_on="store_id",
            right_on="store_fk")
        if all_data.empty:
            return 0
        json_policies = self.oos_policies.copy()
        json_policies[Const.POLICY] = self.oos_policies[Const.POLICY].apply(
            lambda line: json.loads(line))
        diff_policies = json_policies[
            Const.POLICY].drop_duplicates().reset_index()
        diff_table = json_normalize(diff_policies[Const.POLICY].tolist())

        # remove all lists from df
        diff_table = diff_table.applymap(lambda x: x[0]
                                         if isinstance(x, list) else x)
        for col in diff_table.columns:
            att = all_data.iloc[0][col]
            if att is None:
                return 0
            diff_table = diff_table[diff_table[col] == att]
            all_data = all_data[all_data[col] == att]
        if len(diff_table) > 1:
            Log.warning("There is more than one possible match")
            return 0
        if diff_table.empty:
            return 0
        selected_row = diff_policies.iloc[diff_table.index[0]][Const.POLICY]
        json_policies = json_policies[json_policies[Const.POLICY] ==
                                      selected_row]
        products_to_check = json_policies['product_fk'].tolist()
        products_df = all_data[(
            all_data['product_fk'].isin(products_to_check))][[
                'product_fk', 'facings'
            ]].fillna(0)
        products_df = products_df.groupby('product_fk').sum().reset_index()
        try:
            atomic_pk_sku = self.common_v2.get_kpi_fk_by_kpi_name(
                Const.OOS_SKU_KPI)
        except IndexError:
            Log.warning("There is no matching Kpi fk for kpi name: " +
                        Const.OOS_SKU_KPI)
            return 0
        for product in products_to_check:
            if product not in products_df['product_fk'].values:
                products_df = products_df.append(
                    {
                        'product_fk': product,
                        'facings': 0.0
                    }, ignore_index=True)
        for index, row in products_df.iterrows():
            result = 0 if row['facings'] > 0 else 1
            self.common_v2.write_to_db_result(fk=atomic_pk_sku,
                                              numerator_id=row['product_fk'],
                                              numerator_result=row['facings'],
                                              denominator_id=self.store_id,
                                              result=result,
                                              score=result,
                                              identifier_parent=Const.OOS_KPI,
                                              should_enter=True,
                                              parent_fk=3)

        not_existing_products_len = len(
            products_df[products_df['facings'] == 0])
        result = not_existing_products_len / float(len(products_to_check))
        try:
            atomic_pk = self.common_v2.get_kpi_fk_by_kpi_name(Const.OOS_KPI)
            result_oos_pk = self.common_v2.get_kpi_fk_by_kpi_name(
                Const.OOS_RESULT_KPI)
        except IndexError:
            Log.warning("There is no matching Kpi fk for kpi name: " +
                        Const.OOS_KPI)
            return 0
        score = result * weight
        self.common_v2.write_to_db_result(
            fk=atomic_pk,
            numerator_id=self.region_fk,
            numerator_result=not_existing_products_len,
            denominator_id=self.store_id,
            denominator_result=len(products_to_check),
            result=result,
            score=score,
            identifier_result=Const.OOS_KPI,
            parent_fk=3)
        self.common_v2.write_to_db_result(
            fk=result_oos_pk,
            numerator_id=self.region_fk,
            numerator_result=not_existing_products_len,
            denominator_id=self.store_id,
            denominator_result=len(products_to_check),
            result=result,
            score=result,
            parent_fk=3)
        return score

    def save_parent_kpis(self):
        for kpi in self.result_dict.keys():
            try:
                kpi_fk = self.common_v2.get_kpi_fk_by_kpi_name(kpi)
            except IndexError:
                Log.warning("There is no matching Kpi fk for kpi name: " + kpi)
                continue
            if kpi not in self.hierarchy_dict:
                self.common_v2.write_to_db_result(fk=kpi_fk,
                                                  numerator_id=self.region_fk,
                                                  denominator_id=self.store_id,
                                                  result=self.result_dict[kpi],
                                                  score=self.result_dict[kpi],
                                                  identifier_result=kpi,
                                                  parent_fk=1)
            else:
                self.common_v2.write_to_db_result(
                    fk=kpi_fk,
                    numerator_id=self.region_fk,
                    denominator_id=self.store_id,
                    result=self.result_dict[kpi],
                    score=self.result_dict[kpi],
                    identifier_result=kpi,
                    identifier_parent=self.hierarchy_dict[kpi],
                    should_enter=True,
                    parent_fk=2)

    def handle_atomic(self, row):
        result = 0
        atomic_id = row[Const.TEMPLATE_KPI_ID]
        atomic_name = row[Const.KPI_LEVEL_3].strip()
        kpi_name = row[Const.KPI_LEVEL_2].strip()
        set_name = row[Const.KPI_LEVEL_1].strip()
        kpi_type = row[Const.TEMPLATE_KPI_TYPE].strip()
        if atomic_name != kpi_name:
            parent_name = kpi_name
        else:
            parent_name = set_name
        if kpi_type == Const.SOS_TARGET:
            if self.scene_info['number_of_probes'].sum() > 1:
                result = self.handle_sos_target_atomics(
                    atomic_id, atomic_name, parent_name)
        elif kpi_type == Const.SURVEY:
            result = self.handle_survey_atomics(atomic_id, atomic_name,
                                                parent_name)
        elif kpi_type == Const.SURVEY_COMBO:
            result = self.handle_survey_combo(atomic_id, atomic_name,
                                              parent_name)
        elif kpi_type == Const.OOS_KPI:
            result = self.calculate_oos_target()

        # Update kpi results
        if atomic_name != kpi_name:
            if kpi_name not in self.result_dict.keys():
                self.result_dict[kpi_name] = result
                self.hierarchy_dict[kpi_name] = set_name
            else:
                self.result_dict[kpi_name] += result

        # Update set results
        if set_name not in self.result_dict.keys():
            self.result_dict[set_name] = result
        else:
            self.result_dict[set_name] += result

    def handle_sos_target_atomics(self, atomic_id, atomic_name, parent_name):

        denominator_number_of_total_facings = 0
        count_result = -1

        # bring the kpi rows from the sos sheet
        rows = self.sos_target_sheet.loc[self.sos_target_sheet[
            Const.TEMPLATE_KPI_ID] == atomic_id]

        # get a single row
        row = self.find_row(rows)
        if row.empty:
            return 0

        target = row[Const.TEMPLATE_TARGET_PRECENT].values[0]
        score = row[Const.TEMPLATE_SCORE].values[0]
        df = pd.merge(self.scif,
                      self.store_info,
                      how="left",
                      left_on="store_id",
                      right_on="store_fk")

        # get the filters
        filters = self.get_filters_from_row(row.squeeze())
        numerator_number_of_facings = self.count_of_facings(df, filters)
        if numerator_number_of_facings != 0 and count_result == -1:
            if 'manufacturer_name' in filters.keys():
                deno_manufacturer = row[
                    Const.TEMPLATE_MANUFACTURER_DENOMINATOR].values[0].strip()
                deno_manufacturer = deno_manufacturer.split(",")
                filters['manufacturer_name'] = [
                    item.strip() for item in deno_manufacturer
                ]
                denominator_number_of_total_facings = self.count_of_facings(
                    df, filters)
                percentage = 100 * (numerator_number_of_facings /
                                    denominator_number_of_total_facings)
                count_result = score if percentage >= target else -1

        if count_result == -1:
            return 0

        try:
            atomic_pk = self.common_v2.get_kpi_fk_by_kpi_name(atomic_name)
        except IndexError:
            Log.warning("There is no matching Kpi fk for kpi name: " +
                        atomic_name)
            return 0

        self.common_v2.write_to_db_result(
            fk=atomic_pk,
            numerator_id=self.region_fk,
            numerator_result=numerator_number_of_facings,
            denominator_id=self.store_id,
            denominator_result=denominator_number_of_total_facings,
            result=count_result,
            score=count_result,
            identifier_result=atomic_name,
            identifier_parent=parent_name,
            should_enter=True,
            parent_fk=3)
        return count_result

    def find_row(self, rows):
        temp = rows[Const.TEMPLATE_STORE_TYPE]
        rows_stores_filter = rows[(
            temp.apply(lambda r: self.store_type_filter in
                       [item.strip() for item in r.split(",")])) |
                                  (temp == "")]
        temp = rows_stores_filter[Const.TEMPLATE_REGION]
        rows_regions_filter = rows_stores_filter[(
            temp.apply(lambda r: self.region_name_filter in
                       [item.strip() for item in r.split(",")])) |
                                                 (temp == "")]
        temp = rows_regions_filter[Const.TEMPLATE_ADDITIONAL_ATTRIBUTE_6]
        rows_att6_filter = rows_regions_filter[(
            temp.apply(lambda r: self.att6_filter in
                       [item.strip() for item in r.split(",")])) |
                                               (temp == "")]
        return rows_att6_filter

    def get_filters_from_row(self, row):
        filters = dict(row)

        # no need to be accounted for
        for field in Const.DELETE_FIELDS:
            if field in filters:
                del filters[field]

        # filter all the empty cells
        for key in filters.keys():
            if (filters[key] == ""):
                del filters[key]
            elif isinstance(filters[key], tuple):
                filters[key] = (filters[key][0].split(","), filters[key][1])
            else:
                filters[key] = filters[key].split(",")
                filters[key] = [item.strip() for item in filters[key]]

        return self.create_filters_according_to_scif(filters)

    def create_filters_according_to_scif(self, filters):
        convert_from_scif = {
            Const.TEMPLATE_GROUP: 'template_group',
            Const.TEMPLATE_MANUFACTURER_NOMINATOR: 'manufacturer_name',
            Const.TEMPLATE_ADDITIONAL_ATTRIBUTE_6: 'additional_attribute_6'
        }

        for key in filters.keys():
            if key in convert_from_scif:
                filters[convert_from_scif[key]] = filters.pop(key)
        return filters

    def count_of_facings(self, df, filters):

        facing_data = df[self.tools.get_filter_condition(df, **filters)]
        number_of_facings = facing_data['facings'].sum()
        return number_of_facings

    def handle_survey_combo(self, atomic_id, atomic_name, parent_name):
        # bring the kpi rows from the survey sheet
        numerator = denominator = 0
        rows = self.survey_combo_sheet.loc[self.survey_combo_sheet[
            Const.TEMPLATE_KPI_ID] == atomic_id]
        temp = rows[Const.TEMPLATE_STORE_TYPE]
        row_store_filter = rows[(
            temp.apply(lambda r: self.store_type_filter in
                       [item.strip() for item in r.split(",")])) |
                                (temp == "")]
        if row_store_filter.empty:
            return 0

        condition = row_store_filter[Const.TEMPLATE_CONDITION].values[0]
        condition_type = row_store_filter[
            Const.TEMPLATE_CONDITION_TYPE].values[0]
        score = row_store_filter[Const.TEMPLATE_SCORE].values[0]

        # find the answer to the survey in session
        for i, row in row_store_filter.iterrows():
            question_text = row[Const.TEMPLATE_SURVEY_QUESTION_TEXT]
            question_answer_template = row[Const.TEMPLATE_TARGET_ANSWER]

            survey_result = self.survey.get_survey_answer(
                ('question_text', question_text))
            if not survey_result:
                continue
            if '-' in question_answer_template:
                numbers = question_answer_template.split('-')
                try:
                    numeric_survey_result = int(survey_result)
                except:
                    Log.warning("Survey question - " + str(question_text) +
                                " - doesn't have a numeric result")
                    continue
                if numeric_survey_result < int(
                        numbers[0]) or numeric_survey_result > int(numbers[1]):
                    continue
                numerator_or_denominator = row_store_filter[
                    Const.NUMERATOR_OR_DENOMINATOR].values[0]
                if numerator_or_denominator == Const.DENOMINATOR:
                    denominator += numeric_survey_result
                else:
                    numerator += numeric_survey_result
            else:
                continue
        if condition_type == '%':
            if denominator != 0:
                fraction = 100 * (float(numerator) / float(denominator))
            else:
                if numerator > 0:
                    fraction = 100
                else:
                    fraction = 0
            result = score if fraction >= condition else 0
        else:
            return 0

        try:
            atomic_pk = self.common_v2.get_kpi_fk_by_kpi_name(atomic_name)
        except IndexError:
            Log.warning("There is no matching Kpi fk for kpi name: " +
                        atomic_name)
            return 0
        self.common_v2.write_to_db_result(fk=atomic_pk,
                                          numerator_id=self.region_fk,
                                          numerator_result=numerator,
                                          denominator_result=denominator,
                                          denominator_id=self.store_id,
                                          result=result,
                                          score=result,
                                          identifier_result=atomic_name,
                                          identifier_parent=parent_name,
                                          should_enter=True,
                                          parent_fk=3)
        return result

    def handle_survey_atomics(self, atomic_id, atomic_name, parent_name):
        # bring the kpi rows from the survey sheet
        rows = self.survey_sheet.loc[self.survey_sheet[Const.TEMPLATE_KPI_ID]
                                     == atomic_id]
        temp = rows[Const.TEMPLATE_STORE_TYPE]
        row_store_filter = rows[(
            temp.apply(lambda r: self.store_type_filter in
                       [item.strip() for item in r.split(",")])) |
                                (temp == "")]

        if row_store_filter.empty:
            return 0
        else:
            # find the answer to the survey in session
            question_text = row_store_filter[
                Const.TEMPLATE_SURVEY_QUESTION_TEXT].values[0]
            question_answer_template = row_store_filter[
                Const.TEMPLATE_TARGET_ANSWER].values[0]
            score = row_store_filter[Const.TEMPLATE_SCORE].values[0]

            survey_result = self.survey.get_survey_answer(
                ('question_text', question_text))
            if not survey_result:
                return 0
            if '-' in question_answer_template:
                numbers = question_answer_template.split('-')
                try:
                    numeric_survey_result = int(survey_result)
                except:
                    Log.warning("Survey question - " + str(question_text) +
                                " - doesn't have a numeric result")
                    return 0
                if numeric_survey_result < int(
                        numbers[0]) or numeric_survey_result > int(numbers[1]):
                    return 0
                condition = row_store_filter[
                    Const.TEMPLATE_CONDITION].values[0]
                if condition != "":
                    second_question_text = row_store_filter[
                        Const.TEMPLATE_SECOND_SURVEY_QUESTION_TEXT].values[0]
                    second_survey_result = self.survey.get_survey_answer(
                        ('question_text', second_question_text))
                    if not second_survey_result:
                        second_survey_result = 0
                    second_numeric_survey_result = int(second_survey_result)
                    survey_result = 1 if numeric_survey_result >= second_numeric_survey_result else -1
                else:
                    survey_result = 1
            else:
                question_answer_template = question_answer_template.split(',')
                question_answer_template = [
                    item.strip() for item in question_answer_template
                ]
                if survey_result in question_answer_template:
                    survey_result = 1
                else:
                    survey_result = -1
        final_score = score if survey_result == 1 else 0

        try:
            atomic_pk = self.common_v2.get_kpi_fk_by_kpi_name(atomic_name)
        except IndexError:
            Log.warning("There is no matching Kpi fk for kpi name: " +
                        atomic_name)
            return 0
        self.common_v2.write_to_db_result(fk=atomic_pk,
                                          numerator_id=self.region_fk,
                                          numerator_result=0,
                                          denominator_result=0,
                                          denominator_id=self.store_id,
                                          result=survey_result,
                                          score=final_score,
                                          identifier_result=atomic_name,
                                          identifier_parent=parent_name,
                                          should_enter=True,
                                          parent_fk=3)
        return final_score

    def get_new_kpi_static_data(self):
        """
            This function extracts the static new KPI data (new tables) and saves it into one global data frame.
            The data is taken from static.kpi_level_2.
            """
        query = INBEVMXQueries.get_new_kpi_data()
        kpi_static_data = pd.read_sql_query(query, self.rds_conn.db)
        return kpi_static_data
class ComidasToolBox(GlobalSessionToolBox):
    def __init__(self, data_provider, output, common):
        GlobalSessionToolBox.__init__(self, data_provider, output, common)
        self.ps_data_provider = PsDataProvider(data_provider)
        self.own_manufacturer = int(self.data_provider.own_manufacturer.param_value.values[0])
        self.all_templates = self.data_provider[Data.ALL_TEMPLATES]
        self.project_templates = {}
        self.parse_template()
        self.store_type = self.store_info['store_type'].iloc[0]
        self.survey = Survey(self.data_provider, output, ps_data_provider=self.ps_data_provider, common=self.common)
        self.att2 = self.store_info['additional_attribute_2'].iloc[0]
        self.results_df = pd.DataFrame(columns=['kpi_name', 'kpi_fk', 'numerator_id', 'numerator_result',
                                                'denominator_id', 'denominator_result', 'result', 'score',
                                                'identifier_result', 'identifier_parent', 'should_enter'])

        self.products = self.data_provider[Data.PRODUCTS]
        scif = self.scif[['brand_fk', 'facings', 'product_type']].groupby(by='brand_fk').sum()
        self.mpis = self.matches \
            .merge(self.products, on='product_fk', suffixes=['', '_p']) \
            .merge(self.scene_info, on='scene_fk', suffixes=['', '_s']) \
            .merge(self.all_templates[['template_fk', TEMPLATE_GROUP]], on='template_fk') \
            .merge(scif, on='brand_fk')[COLUMNS]
        self.mpis['store_fk'] = self.store_id

        self.calculations = {
            COMBO: self.calculate_combo,
            POSM_AVAILABILITY: self.calculate_posm_availability,
            SCORING: self.calculate_scoring,
            SHARE_OF_EMPTY: self.calculate_share_of_empty,
            SOS: self.calculate_sos,
            SURVEY: self.calculate_survey,
        }

    def parse_template(self):
        for sheet in SHEETS:
            self.project_templates[sheet] = pd.read_excel(TEMPLATE_PATH, sheet_name=sheet)

    def main_calculation(self):
        if not self.store_type == 'Fondas-Rsr':
            return

        relevant_kpi_template = self.project_templates[KPIS]
        sos_kpi_template = self.filter_df(relevant_kpi_template, filters={KPI_TYPE: SOS})
        soe_kpi_template = self.filter_df(relevant_kpi_template, filters={KPI_TYPE: SHARE_OF_EMPTY})
        survey_kpi_template = self.filter_df(relevant_kpi_template, filters={KPI_TYPE: SURVEY})
        posm_kpi_template = self.filter_df(relevant_kpi_template, filters={KPI_TYPE: POSM_AVAILABILITY})
        combo_kpi_template = self.filter_df(relevant_kpi_template, filters={KPI_TYPE: COMBO})
        scoring_kpi_template = self.filter_df(relevant_kpi_template, filters={KPI_TYPE: SCORING})
        sub_scoring_kpi_template = self.filter_df(scoring_kpi_template, filters={KPI_NAME: scoring_kpi_template[PARENT_KPI]}, exclude=True)
        meta_scoring_kpi_template = self.filter_df(scoring_kpi_template, filters={KPI_NAME: scoring_kpi_template[PARENT_KPI]})

        self._calculate_kpis_from_template(sos_kpi_template)
        self._calculate_kpis_from_template(soe_kpi_template)
        self._calculate_kpis_from_template(survey_kpi_template)
        self.calculate_distribution()
        self._calculate_kpis_from_template(posm_kpi_template)
        self._calculate_kpis_from_template(sub_scoring_kpi_template)
        self._calculate_kpis_from_template(combo_kpi_template)
        self._calculate_kpis_from_template(meta_scoring_kpi_template)
        self.save_results_to_db()

    def _calculate_kpis_from_template(self, template_df):
        for i, row in template_df.iterrows():
            calculation_function = self.calculations.get(row[KPI_TYPE])
            try:
                kpi_row = self.project_templates[row[KPI_TYPE]][
                    self.project_templates[row[KPI_TYPE]][KPI_NAME].str.encode('utf-8') == row[KPI_NAME].encode('utf-8')
                    ].iloc[0]
            except IndexError:
                return

            result_data = calculation_function(kpi_row)
            if result_data:
                weight = row['Score']
                if weight and pd.notna(weight) and pd.notna(result_data['result']) and 'score' not in result_data:
                    result_data['score'] = weight * result_data['result']
                parent_kpi_name = self._get_parent_name_from_kpi_name(result_data['kpi_name'])
                if parent_kpi_name and 'identifier_parent' not in result_data.keys():
                    result_data['identifier_parent'] = parent_kpi_name
                if 'identifier_result' not in result_data:
                    result_data['identifier_result'] = result_data['kpi_name']
                if result_data['result'] <= 1:
                    result_data['result'] = result_data['result'] * 100
                if 'numerator_id' not in result_data:
                    result_data['numerator_id'] = self.own_manufacturer
                if 'denominator_id' not in result_data:
                    result_data['denominator_id'] = self.store_id
                self.results_df.loc[len(self.results_df), result_data.keys()] = result_data

    def calculate_distribution(self):
        distribution_template = self.project_templates[DISTRIBUTION] \
            .rename(columns={'store_additional_attribute_2': 'store_size'})
        distribution_template['additional_brands'] = distribution_template \
            .apply(lambda row: int(row['constraint'].split()[0]), axis=1)

        kpi_name = distribution_template.at[0, KPI_NAME]
        kpi_id = self.common.get_kpi_fk_by_kpi_name(kpi_name)

        # anchor_brands = self.sanitize_values(distribution_template.at[0, 'a_value'])
        try:
            anchor_brands = [int(brand) for brand in distribution_template.at[0, 'a_value'].split(",")]
        except AttributeError:
            anchor_brands = [distribution_template.at[0, 'a_value']]

        try:
            template_groups = [template_group.strip() for template_group in distribution_template.at[0, TEMPLATE_GROUP].split(',')]
        except AttributeError:
            template_groups = [distribution_template.at[0, TEMPLATE_GROUP]]

        anchor_threshold = distribution_template.at[0, 'a_test_threshold_2']
        anchor_df = self.filter_df(self.mpis, filters={TEMPLATE_GROUP: template_groups, 'brand_fk': anchor_brands})
        if (anchor_df['facings'] >= anchor_threshold).empty:
            score = result = 0

        try:
            target_brands = [int(brand) for brand in distribution_template.at[0, 'b_value'].split(",")]
        except AttributeError:
            target_brands = [distribution_template.at[0, 'b_value']]

        target_threshold = distribution_template.at[0, 'b_threshold_2']
        target_df = self.filter_df(self.mpis, filters={TEMPLATE_GROUP: template_groups, 'brand_fk': target_brands})
        num_target_brands = len(target_df[target_df['facings'] >= target_threshold]['brand_fk'].unique())
        store_size = self.store_info.at[0, 'additional_attribute_2']

        distribution = self.filter_df(
            distribution_template,
            filters={'additional_brands': num_target_brands, 'store_size': store_size})

        if distribution.empty:
            max_constraints = distribution_template \
                .groupby(by=['store_size'], as_index=False) \
                .max()
            distribution = self.filter_df(max_constraints, filters={'store_size': store_size})

        score = distribution.iloc[0]['Score']
        parent_kpi = distribution.iloc[0][PARENT_KPI]
        max_score = self.filter_df(self.project_templates[KPIS], filters={KPI_NAME: parent_kpi}).iloc[0]['Score']
        result = score / max_score * 100
        numerator_result = len(self.filter_df(self.mpis, filters={
            TEMPLATE_GROUP: template_groups,
            'manufacturer_fk': self.own_manufacturer,
            'product_type': 'SKU'}))
        denominator_result = len(self.filter_df(self.mpis, filters={
            TEMPLATE_GROUP: template_groups,
            'product_type': ['SKU', 'Irrelevant']}))

        result_dict = {
            'kpi_name': kpi_name,
            'kpi_fk': kpi_id,
            'numerator_id': self.own_manufacturer,
            'numerator_result': numerator_result,
            'denominator_id': self.store_id,
            'denominator_result': denominator_result,
            'result': result,
            'score': score,
            'identifier_parent': parent_kpi,
            'identifier_result': kpi_name
        }

        self.results_df.loc[len(self.results_df), result_dict.keys()] = result_dict

    def calculate_share_of_empty(self, row):
        target = row['target']
        numerator_param1 = row[NUMERATOR_PARAM_1]
        numerator_value1 = row[NUMERATOR_VALUE_1]

        kpi_name = row[KPI_NAME]
        kpi_id = self.common.get_kpi_fk_by_kpi_name(kpi_name)
        template_groups = row[TEMPLATE_GROUP].split(',')
        denominator_scif = self.filter_df(self.scif, filters={TEMPLATE_GROUP: template_groups})
        denominator_scif = self.filter_df(denominator_scif, filters={'product_type': 'POS'}, exclude=True)
        numerator_scif = self.filter_df(denominator_scif, filters={numerator_param1: numerator_value1})
        template_id = self.filter_df(self.all_templates, filters={TEMPLATE_GROUP: template_groups})['template_fk'].unique()[0]

        result_dict = {
            'kpi_name': kpi_name,
            'kpi_fk': kpi_id,
            'numerator_id': self.own_manufacturer,
            'denominator_id': template_id,
            'result': 0}

        if not numerator_scif.empty:
            denominator_result = denominator_scif.facings.sum()
            numerator_result = numerator_scif.facings.sum()
            result = (numerator_result / denominator_result)
            result_dict['numerator_result'] = numerator_result
            result_dict['denominator_result'] = denominator_result
            result_dict['result'] = self.calculate_sos_score(target, result)

        return result_dict

    def calculate_sos(self, row):
        kpi_name = row[KPI_NAME]
        kpi_id = self.common.get_kpi_fk_by_kpi_name(kpi_name)
        template_groups = self.sanitize_values(row[TEMPLATE_GROUP])
        product_types = row['product_type'].split(",")

        den_df = self.filter_df(self.mpis, filters={TEMPLATE_GROUP: template_groups, 'product_type': product_types})
        num_param = row[NUMERATOR_PARAM_1]
        num_val = row[NUMERATOR_VALUE_1]
        num_df = self.filter_df(den_df, filters={num_param: num_val})

        try:
            ratio = len(num_df) / len(den_df)
        except ZeroDivisionError:
            ratio = 0

        target = row['target']
        result = self.calculate_sos_score(target, ratio)

        result_dict = {
            'kpi_name': kpi_name,
            'kpi_fk': kpi_id,
            'numerator_id': self.own_manufacturer,
            'denominator_id': num_df[row[DENOMINATOR_ENTITY]].mode().iloc[0],
            'result': result
        }

        return result_dict

    def calculate_posm_availability(self, row):
        # if dominant kpi passed, skip
        result = 100
        max_score = row['KPI Total Points']
        if row['Dominant KPI'] != 'Y':
            result = 50
            dom_kpi = self.filter_df(self.project_templates['POSM Availability'],
                                     filters={'Parent KPI': row['Parent KPI'], 'Dominant KPI': 'Y'}
                                     )
            dom_name = dom_kpi.iloc[0][KPI_NAME]
            max_score = dom_kpi.iloc[0]['KPI Total Points']
            dom_score = self.filter_df(self.results_df, filters={'kpi_name': dom_name}).iloc[0]['result']
            if dom_score > 0:
                result = 0

        kpi_name = row['KPI Name']
        kpi_fk = self.common.get_kpi_fk_by_kpi_name(kpi_name)
        product_fks = [int(product) for product in str(row['product_fk']).split(',')]
        template_fks = self.get_template_fk(row['template_name'])
        filtered_df = self.filter_df(self.mpis, filters={'template_fk': template_fks, 'product_fk': product_fks})

        if filtered_df.empty:
            result = 0

        score = max_score * result / 100

        try:
            denominator_id = filtered_df['template_fk'].mode().iloc[0]
        except IndexError:
            denominator_id = template_fks[0]

        result_dict = {
            'kpi_fk': kpi_fk,
            'kpi_name': kpi_name,
            'denominator_id': denominator_id,
            'result': result,
            'score': score,
        }

        return result_dict

    def calculate_survey(self, row):
        """
        Determines whether the calculation passes based on if the survey response in `row` is 'Si' or 'No'.

        :param row: Row of template containing Survey question data.
        :return: Dictionary containing KPI results.
        """

        kpi_name = row[KPI_NAME]
        kpi_id = self.common.get_kpi_fk_by_kpi_name(kpi_name)
        result = 1 if self.survey.get_survey_answer(row['KPI Question']).lower() == 'si' else 0
        result_dict = {
            'kpi_name': kpi_name,
            'kpi_fk': kpi_id,
            'numerator_id': self.own_manufacturer,
            'denominator_id': self.store_id,
            'result': result
        }

        return result_dict

    def calculate_scoring(self, row):
        kpi_name = row[KPI_NAME]
        kpi_id = self.common.get_kpi_fk_by_kpi_name(kpi_name)
        component_kpi = [comp.strip() for comp in row['Component KPIs'].split(',')]
        component_df = self.filter_df(self.results_df, filters={'kpi_name': component_kpi})
        score = component_df['score'].sum()
        result = score if kpi_name == "ICE-Fondas-Rsr" else score / row['Score'] * 100

        result_dict = {
            'kpi_name': kpi_name,
            'kpi_fk': kpi_id,
            'numerator_id': self.own_manufacturer,
            'denominator_id': self.store_id,
            'result': result,
            'score': score,
        }

        return result_dict

    def calculate_combo(self, row):
        kpi_name = row[KPI_NAME]
        kpi_id = self.common.get_kpi_fk_by_kpi_name(kpi_name)

        a_filter = row['a_filter']
        a_value = row['a_value']

        component_kpi = [comp.strip() for comp in self.filter_df(self.project_templates['Scoring'], filters={KPI_NAME: a_value}).iloc[0]['Component KPIs'].split(",")]
        component_df = self.filter_df(self.results_df, filters={'kpi_name': component_kpi})
        a_test = row['a_test']
        a_score = component_df[a_test].sum()
        a_threshold = row['a_threshold']
        a_check = a_score >= a_threshold

        template_groups = row[TEMPLATE_GROUP]
        b_filter = row['b_filter']
        b_value = row['b_value'].split(",")
        b_threshold = row['b_threshold']
        b_check = len(self.filter_df(self.mpis, filters={TEMPLATE_GROUP: template_groups, b_filter: b_value})) >= b_threshold

        func = LOGIC.get(row['b_logic'].lower())
        result = int(func(a_check, b_check))

        result_dict = {
            'kpi_name': kpi_name,
            'kpi_fk': kpi_id,
            'result': result,
        }

        return result_dict

    # def calculate_scoring(self, row):
    #     kpi_name = row[KPI_NAME]
    #     kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name)
    #     numerator_id = self.own_manuf_fk
    #     denominator_id = self.store_id
    #
    #     result_dict = {'kpi_name': kpi_name, 'kpi_fk': kpi_fk, 'numerator_id': numerator_id,
    #                    'denominator_id': denominator_id}
    #
    #     component_kpis = self.sanitize_values(row['Component KPIs'])
    #     dependency_kpis = self.sanitize_values(row['Dependency'])
    #     relevant_results = self.results_df[self.results_df['kpi_name'].isin(component_kpis)]
    #     passing_results = relevant_results[(relevant_results['result'] != 0) &
    #                                        (relevant_results['result'].notna()) &
    #                                        (relevant_results['score'] != 0)]
    #     nan_results = relevant_results[relevant_results['result'].isna()]
    #     if len(relevant_results) > 0 and len(relevant_results) == len(nan_results):
    #         result_dict['result'] = pd.np.nan
    #     elif row['Component aggregation'] == 'one-passed':
    #         if len(relevant_results) > 0 and len(passing_results) > 0:
    #             result_dict['result'] = 1
    #         else:
    #             result_dict['result'] = 0
    #     elif row['Component aggregation'] == 'sum':
    #         if len(relevant_results) > 0:
    #             result_dict['score'] = relevant_results['score'].sum()
    #             if 'result' not in result_dict.keys():
    #                 if row['score_based_result'] == 'y':
    #                     result_dict['result'] = 0 if result_dict['score'] == 0 else result_dict['score'] / row['Score']
    #                 elif row['composition_based_result'] == 'y':
    #                     result_dict['result'] = 0 if passing_results.empty else float(len(passing_results)) / len(
    #                         relevant_results)
    #                 else:
    #                     result_dict['result'] = result_dict['score']
    #         else:
    #             result_dict['score'] = 0
    #             if 'result' not in result_dict.keys():
    #                 result_dict['result'] = result_dict['score']
    #     if dependency_kpis and dependency_kpis is not pd.np.nan:
    #         dependency_results = self.results_df[self.results_df['kpi_name'].isin(dependency_kpis)]
    #         passing_dependency_results = dependency_results[dependency_results['result'] != 0]
    #         if len(dependency_results) > 0 and len(dependency_results) == len(passing_dependency_results):
    #             result_dict['result'] = 1
    #         else:
    #             result_dict['result'] = 0
    #
    #     return result_dict

    def _filter_df_based_on_row(self, row, df):
        columns_in_scif = row.index[np.in1d(row.index, df.columns)]
        for column_name in columns_in_scif:
            if pd.notna(row[column_name]):
                df = df[df[column_name].isin(self.sanitize_values(row[column_name]))]
            if df.empty:
                break
        return df

    def _get_kpi_name_and_fk(self, row, generic_num_dem_id=False):
        kpi_name = row[KPI_NAME]
        kpi_fk = self.common.get_kpi_fk_by_kpi_type(kpi_name)
        output = [kpi_name, kpi_fk]
        if generic_num_dem_id:
            numerator_id = self.scif[row[NUMERATOR_ENTITY]].mode().iloc[0]
            denominator_id = self.scif[row[DENOMINATOR_ENTITY]].mode().iloc[0]
            output.append(numerator_id)
            output.append(denominator_id)
        return output

    def _get_parent_name_from_kpi_name(self, kpi_name):
        template = self.project_templates[KPIS]
        parent_kpi_name = \
            template[template[KPI_NAME].str.encode('utf-8') == kpi_name.encode('utf-8')][PARENT_KPI].iloc[0]
        if parent_kpi_name and pd.notna(parent_kpi_name):
            return parent_kpi_name
        else:
            return None

    @staticmethod
    def sanitize_values(item):
        if pd.isna(item):
            return item
        else:
            if type(item) == int:
                return str(item)
            else:
                items = [item.strip() for item in item.split(',')]
                return items

    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.filter_df(self.results_df, filters={'identifier_parent': None}, func=pd.Series.notnull)['should_enter'] = True
        # self.results_df.loc[self.results_df['identifier_parent'].notnull(), 'should_enter'] = True
        # set result to NaN for records that do not have a parent
        # identifier_results = self.results_df[self.results_df['result'].notna()]['identifier_result'].unique().tolist()
        # self.results_df['result'] = self.results_df.apply(
        #     lambda row: pd.np.nan if (pd.notna(row['identifier_parent']) and row[
        #         'identifier_parent'] not in identifier_results) else row['result'], axis=1)
        self.results_df['result'] = self.results_df.apply(
            lambda row: row['result'] if (
                    pd.notna(row['identifier_parent']) or pd.notna(row['identifier_result'])) else np.nan, axis=1)
        # get rid of 'not applicable' results
        self.results_df.dropna(subset=['result'], inplace=True)
        self.results_df.fillna(0, inplace=True)
        results = self.results_df.to_dict('records')
        for result in results:
            self.write_to_db(**result)

    @staticmethod
    def calculate_sos_score(target, result):
        """
        Determines whether `result` is greater than or within the range of `target`.

        :param target: Target value as either a minimum value or a '-'-separated range.
        :param result: Calculation result to compare to 1target1.
        :return: 1 if `result` >= `target` or is within the `target` range.
        """

        score = 0
        if pd.notna(target):
            target = [int(n) for n in str(target).split('-')]  # string cast redundant?
            if len(target) == 1:
                score = int(result*100 >= target[0])
            if len(target) == 2:
                score = int(target[0] <= result*100 <= target[1])
        return score

    @staticmethod
    def filter_df(df, filters, exclude=False, func=pd.Series.isin):
        """
        :param df: DataFrame to filter.
        :param filters: Dictionary of column-value list pairs to filter by.
        :param exclude:
        :param func: Function to determine inclusion.
        :return: Filtered DataFrame.
        """

        vert = op.inv if exclude else op.pos
        func = LOGIC.get(func, func)

        for col, val in filters.items():
            if not hasattr(val, '__iter__'):
                val = [val]
            try:
                if isinstance(val, pd.Series) and val.any() or pd.notna(val[0]):
                    df = df[vert(func(df[col], val))]
            except TypeError:
                df = df[vert(func(df[col]))]
        return df

    def get_template_fk(self, template_name):
        """
        :param template_name: Name of template.
        :return: ID of template.
        """

        template_df = self.filter_df(self.all_templates, filters={'template_name': template_name})
        template_fks = template_df['template_fk'].unique()

        return template_fks