← Files ClaraARCHIVED FILE

modules/distribution-analysis/vendor/modules/data/cohort_processing.py

15.9 KB · Oct 5, 2026 · 00:02 UTC

↓ Download file

import copy
import logging
from itertools import product

import polars as pl
import psutil

from modules.data.data_cleaning import (
    check_date_and_group_data,
    get_count_metric_names,
    manage_filtering,
    query_filter_dataframe_all_periods,
    query_filter_dataframe_dates,
    query_filter_dataframe_periods,
    query_filter_dataframe_plan,
)
from modules.layout.set_up_widgets import (
    download_filtered_file,
    set_up_cohort_column_widget,
    set_up_count_metrics_widget,
)
from modules.utilities.config import get_naming_params
from modules.utilities.error_messages import add_app_message_to_paramdict
from modules.utilities.helpers import (
    measure_time,
    unique,
)
from modules.utilities.ui_notifier import ui as notifier
from modules.utilities.utils import (
    ensure_lazyframe,
    get_row_count,
    get_schema_and_column_names,
    is_valid_lazyframe,
)


def prepare_cohort_and_period_data_for_analysis(
    paramDictCopy,
    df,
    dfDates,
    dfPeriods,
    dfAllPeriods,
    dfPlan,
    dfDict,
    colDict,
    tabDict,
    chartDict,
    automateDict,
    planPlaybackDict,
):
    namingParams = get_naming_params()
    errorMessageType = namingParams["errorMessageType"]
    impossibleToProcessFile = namingParams["impossibleToProcessFile"]
    processingChoice = namingParams["processingChoice"]
    dataPreparation = namingParams["dataPreparationName"]
    loadDataTabKey = namingParams["loadDataTab"]
    plotChartsTabKey = namingParams["plotChartsTab"]
    columnOrder = namingParams["columnOrderName"]
    filterDataTabKey = namingParams["filterDataTab"]
    setVarianceOptionsTabKey = namingParams["setVarianceOptionsTab"]
    checkedDates = namingParams["checkedDatesName"]
    addedLostDroppedCols = namingParams["addedLostDroppedColsName"]
    queriedData = namingParams["queriedDataName"]
    colNumber = 0
    if paramDictCopy[impossibleToProcessFile] and processingChoice in chartDict:

        try:
            columns, _schema = get_schema_and_column_names(df)
        except Exception as e:
            logging.exception(e)
            notifier.error(f"Cohort schema error: {e}")
            columns = []
        try:
            row_count = get_row_count(df)
        except Exception as e:
            logging.exception(e)
            notifier.error(f"Cohort row-count error: {e}")
            row_count = 0
        columns_str = ", ".join(columns) if columns else "None"
        parse_msg = paramDictCopy.get("fileParseError", "")

        if not is_valid_lazyframe(df):
            message = (parse_msg + " ") if parse_msg else ""
            message += (
                "Empty or not processable dataset "
                f"(columns={columns_str}, rows={row_count}). "
                "Click on 🔍Detected columns to see which metric columns have been mapped."
            )
            paramDict = add_app_message_to_paramdict(
                message,
                errorMessageType,
                loadDataTabKey,
                paramDictCopy,
                isMessage=True,
                isToast=True,
                colNumber=colNumber,
            )
        else:
            message = (parse_msg + " ") if parse_msg else ""
            message += (
                "Unable to detect required columns in dataset "
                f"(columns={columns_str}, rows={row_count}). "
                "Click on 🔍Detected columns to see which metric columns have been mapped."
            )
            paramDict = add_app_message_to_paramdict(
                message,
                errorMessageType,
                loadDataTabKey,
                paramDictCopy,
                isMessage=True,
                isToast=True,
                colNumber=colNumber,
            )
        return None, None, None, None, None, None, None, paramDictCopy, None, None, None
    elif is_valid_lazyframe(df) and not paramDictCopy[impossibleToProcessFile]:
        paramDict = copy.deepcopy(paramDictCopy)
        df, indexCols, valueCols, paramDict, originalValueColsCopy = (
            check_date_and_group_data(paramDict, df)
        )
        measure_time(dataPreparation, checkedDates, False)
        chartDict = set_up_cohort_column_widget(
            dfAllPeriods,
            paramDict,
            chartDict,
            automateDict,
            indexCols,
            colDict[plotChartsTabKey],
        )
        df, dfDates, dfPeriods, dfAllPeriods, dfPlan, indexCols = add_cohort_column(
            df,
            dfDates,
            dfPeriods,
            dfAllPeriods,
            dfPlan,
            indexCols,
            paramDict,
            chartDict,
        )
        df, dfDates, dfPeriods, dfAllPeriods, dfPlan, indexCols = (
            add_lost_and_dropped_column(
                df,
                dfDates,
                dfPeriods,
                dfAllPeriods,
                dfPlan,
                indexCols,
                paramDict,
                chartDict,
            )
        )
        columns, schema = get_schema_and_column_names(dfPlan)
        paramDict[columnOrder] = columns
        measure_time(dataPreparation, addedLostDroppedCols, False)
        df, indexCols, toDrop, paramDict, chartDict = manage_filtering(
            df,
            indexCols,
            paramDict,
            chartDict,
            automateDict,
            valueCols,
            colDict[filterDataTabKey],
            colDict[setVarianceOptionsTabKey],
        )
        dfDates = query_filter_dataframe_dates(
            dfDates, chartDict[namingParams["filterDictName"]]
        )
        measure_time(
            dataPreparation, "0167 - query_filter_dataframe_dates - Converted", False
        )
        dfPeriods = query_filter_dataframe_periods(
            dfPeriods, chartDict[namingParams["filterDictName"]]
        )
        measure_time(
            dataPreparation, "0168 - query_filter_dataframe_period - Converted", False
        )
        dfAllPeriods = query_filter_dataframe_all_periods(
            dfAllPeriods, chartDict[namingParams["filterDictName"]]
        )
        measure_time(
            dataPreparation,
            "0169 - query_filter_dataframe_all_periods - Converted",
            False,
        )
        dfPlan = query_filter_dataframe_plan(
            dfPlan, chartDict[namingParams["filterDictName"]]
        )
        measure_time(
            dataPreparation, "0170 - query_filter_dataframe_plan - Converted", False
        )
        download_filtered_file(df, dfDates, valueCols, colDict, paramDict)
        chartDict = set_up_count_metrics_widget(
            paramDict, chartDict, automateDict, indexCols, colDict[plotChartsTabKey]
        )
        chartDict = get_count_metric_names(chartDict, valueCols)
    try:
        logger = logging.getLogger(__name__)
        columns_main, _ = get_schema_and_column_names(df)
        column_lookup = {c.lower(): c for c in columns_main}
        normalised_indexCols: list[str] = []
        for col in indexCols:
            resolved = column_lookup.get(col.lower(), col)
            if resolved != col:
                logger.debug(
                    "cohort-processing: normalising index column '%s' -> '%s'",
                    col,
                    resolved,
                )
            normalised_indexCols.append(resolved)
        indexCols = normalised_indexCols
        logger.debug(
            "cohort-processing: final indexCols=%s valueCols=%s",
            indexCols,
            valueCols,
        )
    except Exception:
        pass
    return (
        df,
        dfDates,
        dfPeriods,
        dfAllPeriods,
        dfPlan,
        indexCols,
        valueCols,
        paramDict,
        chartDict,
        toDrop,
        originalValueColsCopy,
    )


def add_lost_and_dropped_column(
    df, dfDates, dfPeriods, dfAllPeriods, dfPlan, indexCols, paramDict, chartDict
):
    namingParams = get_naming_params()
    lostAndDroppedColumnKey = namingParams["lostAndDroppedColumn"]
    periodName = namingParams["periodName"]
    allPeriodsList = namingParams["allPeriodsList"]
    nothingFilteredName = namingParams["nothingFilteredName"]
    workColumn = namingParams["workColumn"]
    lostAndDroppedSuffix = namingParams["lostAndDroppedSuffix"]
    lostName = namingParams["lostName"]
    activeName = namingParams["activeName"]
    if lostAndDroppedColumnKey in chartDict and chartDict[lostAndDroppedColumnKey]:
        if chartDict[lostAndDroppedColumnKey] != nothingFilteredName:
            lostAndDroppedColumn = chartDict[lostAndDroppedColumnKey]
            orderedPeriods = paramDict[allPeriodsList]
            lostAndDroppedColumnName = lostAndDroppedColumn + lostAndDroppedSuffix
            # Start from lazy
            dfLostAndDropped = (
                dfAllPeriods.select([pl.col(lostAndDroppedColumn), pl.col(periodName)])
                .unique()
                .with_columns(pl.lit(1).alias(lostAndDroppedColumnName))
            )
            indexCols.append(lostAndDroppedColumnName)
            # Create one column per period indicating the presence (1) or absence (null)
            dfLostAndDropped = dfLostAndDropped.with_columns(
                [
                    pl.when(pl.col(periodName) == p)
                    .then(pl.col(lostAndDroppedColumnName))
                    .otherwise(None)
                    .alias(p)
                    for p in orderedPeriods
                ]
            )
            # Now aggregate to create the wide format (similar to pivot)
            dfLostAndDropped = dfLostAndDropped.group_by(lostAndDroppedColumn).agg(
                [pl.col(p).max().alias(p) for p in orderedPeriods]
            )
            # Filtering and concatenating
            count = 0
            frameArray = []
            for period in orderedPeriods[:-1]:
                dfPeriod = (
                    dfLostAndDropped.filter(
                        (pl.col(orderedPeriods[count]) == 1)
                        & (pl.col(orderedPeriods[count + 1]).is_null())
                    )
                    .with_columns(
                        pl.lit(lostName + "<br>" + period).alias(
                            lostAndDroppedColumnName
                        )
                    )
                    .drop(orderedPeriods)
                )
                frameArray.append(dfPeriod)
                count += 1

            dfLostAndDropped = pl.concat(frameArray, how="vertical")
            if isinstance(dfLostAndDropped, pl.LazyFrame):
                dfLostAndDropped_lazy = dfLostAndDropped.sort(lostAndDroppedColumn)
                dfLostAndDropped_eager = dfLostAndDropped_lazy.collect()
            else:
                dfLostAndDropped = dfLostAndDropped.sort(lostAndDroppedColumn)
                dfLostAndDropped_lazy = dfLostAndDropped.lazy()
                dfLostAndDropped_eager = dfLostAndDropped

            def _augment(frame):
                if not is_valid_lazyframe(frame):
                    return frame
                if isinstance(frame, pl.LazyFrame):
                    return (
                        frame.sort(lostAndDroppedColumn)
                        .join(
                            dfLostAndDropped_lazy,
                            on=lostAndDroppedColumn,
                            how="left",
                        )
                        .with_columns(
                            pl.col(lostAndDroppedColumnName).fill_null(activeName)
                        )
                    )
                if isinstance(frame, pl.DataFrame):
                    return (
                        frame.sort(lostAndDroppedColumn)
                        .join(
                            dfLostAndDropped_eager,
                            on=lostAndDroppedColumn,
                            how="left",
                        )
                        .with_columns(
                            pl.col(lostAndDroppedColumnName).fill_null(activeName)
                        )
                    )
                return frame

            df = _augment(df)
            dfDates = _augment(dfDates)
            dfPeriods = _augment(dfPeriods)
            dfAllPeriods = _augment(dfAllPeriods)
            dfPlan = _augment(dfPlan)
    return df, dfDates, dfPeriods, dfAllPeriods, dfPlan, indexCols


def add_cohort_column(
    df, dfDates, dfPeriods, dfAllPeriods, dfPlan, indexCols, paramDict, chartDict
):
    namingParams = get_naming_params()
    chosenCohortColumnKey = namingParams["chosenCohortColumn"]
    periodName = namingParams["periodName"]
    allPeriodsList = namingParams["allPeriodsList"]
    nothingFilteredName = namingParams["nothingFilteredName"]
    chosenCohortSuffix = namingParams["chosenCohortSuffix"]
    sinceName = namingParams["sinceName"]

    def is_lazyframe(lf):
        return lf is not None and isinstance(lf, pl.LazyFrame)

    if chosenCohortColumnKey in chartDict and chartDict[chosenCohortColumnKey]:
        if chartDict[chosenCohortColumnKey] != nothingFilteredName:
            chosenCohortColumn = chartDict[chosenCohortColumnKey]
            orderedPeriods = paramDict[allPeriodsList]
            cohortColumnName = chosenCohortColumn + chosenCohortSuffix

            # 1) Map period -> rank
            period2rank = pl.DataFrame(
                {periodName: orderedPeriods, "period_rank": range(len(orderedPeriods))}
            ).lazy()

            # 2) Build dfCohorts
            dfCohorts = (
                dfAllPeriods.select([chosenCohortColumn, periodName])
                .unique()
                .join(period2rank, on=periodName, how="left")
                .group_by(chosenCohortColumn)
                .agg(
                    [
                        # Pick earliest period by sorting on period_rank
                        pl.col(periodName)
                        .sort_by(pl.col("period_rank"))
                        .first()
                        .alias("earliest_period")
                    ]
                )
                .with_columns(
                    (
                        pl.lit(sinceName) + pl.lit("<br>") + pl.col("earliest_period")
                    ).alias(cohortColumnName)
                )
                .drop(["earliest_period"])
            )

            dfCohorts_lazy = ensure_lazyframe(dfCohorts)

            def join_with_cohort_labels(
                target: pl.DataFrame | pl.LazyFrame,
            ) -> pl.DataFrame | pl.LazyFrame:
                """Attach cohort labels to ``target`` preserving its original type."""

                if not is_valid_lazyframe(target):
                    return target

                column_names, _schema = get_schema_and_column_names(target)
                if chosenCohortColumn not in column_names:
                    return target

                target_is_lazy = isinstance(target, pl.LazyFrame)
                joined_lazy = ensure_lazyframe(target).join(
                    dfCohorts_lazy,
                    on=chosenCohortColumn,
                    how="left",
                )
                return joined_lazy if target_is_lazy else joined_lazy.collect()

            # 3) Join that single-col label to each table
            indexCols.append(cohortColumnName)

            if is_valid_lazyframe(df):
                df = join_with_cohort_labels(df)

            columns, schema = get_schema_and_column_names(dfDates)
            if is_valid_lazyframe(dfDates) and chosenCohortColumn in columns:
                dfDates = join_with_cohort_labels(dfDates)

            columns, schema = get_schema_and_column_names(dfPeriods)
            if is_valid_lazyframe(dfPeriods) and chosenCohortColumn in columns:
                dfPeriods = join_with_cohort_labels(dfPeriods)

            columns, schema = get_schema_and_column_names(dfAllPeriods)
            if is_valid_lazyframe(dfAllPeriods) and chosenCohortColumn in columns:
                dfAllPeriods = join_with_cohort_labels(dfAllPeriods)

            columns, schema = get_schema_and_column_names(dfPlan)
            if is_valid_lazyframe(dfPlan) and chosenCohortColumn in columns:
                dfPlan = join_with_cohort_labels(dfPlan)

    return df, dfDates, dfPeriods, dfAllPeriods, dfPlan, indexCols

SHA-256: c400326ca69007552cd5a23ef5e483b28d3362b4886e859528d32017456d3b91