/ Development View source
Projects/CCHS/Code Reference
Development documentation from main. Features may not be released. Read the current release.

Code Reference

Generated API documentation for the modules that implement harmonization, data loading, and bootstrap analysis. The Streamlit UI layer (src/ui/) is presentation glue over these modules and is not documented here.

Harmonization

src.data.harmonizer

Data harmonization functions for multi-cycle CCHS analysis.

build_crosswalk

build_crosswalk(cycles, descriptions, cutoff=0.6)

Build a crosswalk mapping reference-cycle variable names to each cycle's matching variable name, by fuzzy-matching variable descriptions.

The last entry in cycles is treated as the reference cycle: every other cycle's variable is matched against each reference variable's description via difflib.get_close_matches (single best match). This is a heuristic textual match, not an authoritative concordance - unmatched or ambiguous descriptions are recorded as None and should be reviewed.

Parameters:

Name Type Description Default
cycles list

Cycle years in order, with the reference cycle last

required
descriptions dict

Dict mapping cycle -> {variable_name: description}

required
cutoff float

difflib similarity cutoff (0-1) for a match to count

0.6

Returns:

Type Description
dict

Dict mapping reference_var -> {cycle: cycle_specific_var_or_None}

Source code in src/data/harmonizer.py
def build_crosswalk(cycles: list, descriptions: dict, cutoff: float = 0.6) -> dict:
    """
    Build a crosswalk mapping reference-cycle variable names to each cycle's
    matching variable name, by fuzzy-matching variable descriptions.

    The last entry in `cycles` is treated as the reference cycle: every
    other cycle's variable is matched against each reference variable's
    description via difflib.get_close_matches (single best match). This is
    a heuristic textual match, not an authoritative concordance - unmatched
    or ambiguous descriptions are recorded as None and should be reviewed.

    Args:
        cycles: Cycle years in order, with the reference cycle last
        descriptions: Dict mapping cycle -> {variable_name: description}
        cutoff: difflib similarity cutoff (0-1) for a match to count

    Returns:
        Dict mapping reference_var -> {cycle: cycle_specific_var_or_None}
    """
    reference_cycle = cycles[-1]
    reference_vars = descriptions[reference_cycle]

    crosswalk = {}
    for ref_var, ref_desc in reference_vars.items():
        crosswalk[ref_var] = {reference_cycle: ref_var}
        for cycle in cycles[:-1]:
            candidates = descriptions[cycle]
            best_match = difflib.get_close_matches(ref_desc, candidates.values(), n=1, cutoff=cutoff)
            if best_match:
                for var, desc in candidates.items():
                    if desc == best_match[0]:
                        crosswalk[ref_var][cycle] = var
                        break
            else:
                crosswalk[ref_var][cycle] = None

    return crosswalk

auto_harmonize

auto_harmonize(label)

Normalize a raw codebook category label into a shared harmonized category.

Order matters: "not stated", "valid skip", "don't know", and "female" each contain "no" or "male" as a substring (e.g. "not stated", "female"), so the more specific phrases must be checked before the shorter "no"/"male" rules or they get misclassified.

Source code in src/data/harmonizer.py
def auto_harmonize(label: str) -> str:
    """
    Normalize a raw codebook category label into a shared harmonized category.

    Order matters: "not stated", "valid skip", "don't know", and "female"
    each contain "no" or "male" as a substring (e.g. "**no**t stated",
    "fe**male**"), so the more specific phrases must be checked before the
    shorter "no"/"male" rules or they get misclassified.
    """
    l = label.lower()
    if "not stated" in l:
        return "Not stated"
    if "valid skip" in l:
        return "Valid skip"
    if "don’t know" in l or "don't know" in l:
        return "Don't know"
    if "female" in l:
        return "Female"
    if "male" in l:
        return "Male"
    if "yes" in l:
        return "Yes"
    if "no" in l:
        return "No"
    return label.strip()

get_common_harmonized_vars

get_common_harmonized_vars(cycles, crosswalk, data_dict)

Get harmonized variables that exist in all specified cycles.

Parameters:

Name Type Description Default
cycles list

List of cycle years

required
crosswalk dict

Crosswalk dictionary

required
data_dict dict

Dictionary mapping cycle -> DataFrame (with cycle-specific column names)

required

Returns:

Type Description
list

List of harmonized variable names available in all cycles

Source code in src/data/harmonizer.py
def get_common_harmonized_vars(cycles: list, crosswalk: dict, data_dict: dict) -> list:
    """
    Get harmonized variables that exist in all specified cycles.

    Args:
        cycles: List of cycle years
        crosswalk: Crosswalk dictionary
        data_dict: Dictionary mapping cycle -> DataFrame (with cycle-specific column names)

    Returns:
        List of harmonized variable names available in all cycles
    """
    common_vars = []

    for harmonized_var, cycle_mapping in crosswalk.items():
        available_in_all = True
        for cycle in cycles:
            cycle_specific_var = cycle_mapping.get(cycle)
            if not cycle_specific_var or cycle_specific_var not in data_dict[cycle].columns:
                available_in_all = False
                break

        if available_in_all:
            common_vars.append(harmonized_var)

    return common_vars

src.data.codebook_extractor

Parsing logic for Statistics Canada CCHS Data Dictionary/Freqs PDF codebooks.

parse_codebook_lines

parse_codebook_lines(lines)

Parse codebook text lines into a variable -> {description, categories} dict.

Expects the Statistics Canada CCHS Data Dictionary/Freqs layout: each variable starts with a "Variable Name:" line, has a "Concept:" line for its description, and an "Answer Categories" section listing "label code frequency frequency%" rows until a blank line or a Note:/Source:/ Universe:/Total line ends the section.

Parameters:

Name Type Description Default
lines Iterable[str]

An iterable of text lines, in document order (may span multiple PDF pages - state carries across page boundaries the same way it does across lines within a page).

required

Returns:

Type Description
dict

Dict mapping variable_name -> {"description": str, "categories": {code: label}}

Source code in src/data/codebook_extractor.py
def parse_codebook_lines(lines: Iterable[str]) -> dict:
    """
    Parse codebook text lines into a variable -> {description, categories} dict.

    Expects the Statistics Canada CCHS Data Dictionary/Freqs layout: each
    variable starts with a "Variable Name:" line, has a "Concept:" line for
    its description, and an "Answer Categories" section listing "label code
    frequency frequency%" rows until a blank line or a Note:/Source:/
    Universe:/Total line ends the section.

    Args:
        lines: An iterable of text lines, in document order (may span
            multiple PDF pages - state carries across page boundaries the
            same way it does across lines within a page).

    Returns:
        Dict mapping variable_name -> {"description": str, "categories": {code: label}}
    """
    variables = {}
    current_variable = None
    current_description = None
    codes = {}
    in_answer_categories = False

    for line in lines:
        if "Variable Name:" in line:
            if current_variable:
                variables[current_variable] = {
                    "description": current_description,
                    "categories": codes.copy() if codes else {}
                }
            current_variable = line.split(":")[1].strip().replace(" Length", "")
            current_description = None
            codes = {}
            in_answer_categories = False
        elif "Concept:" in line:
            current_description = line.split(":")[1].strip()
        elif "Answer Categories" in line:
            in_answer_categories = True
            continue
        elif in_answer_categories:
            match = CATEGORY_ROW.match(line)
            if match:
                meaning = match.group(1).strip()
                code = match.group(2).strip()
                codes[code] = meaning
            elif line.strip() == "" or SECTION_END.match(line):
                in_answer_categories = False
                continue
        elif "Note:" in line or "Source:" in line or "Universe:" in line:
            in_answer_categories = False
            continue

    if current_variable:
        variables[current_variable] = {
            "description": current_description,
            "categories": codes.copy() if codes else {}
        }

    return variables

extract_variables_with_categories

extract_variables_with_categories(pdf_path)

Extract variable/category metadata from a CCHS codebook PDF.

Source code in src/data/codebook_extractor.py
def extract_variables_with_categories(pdf_path) -> dict:
    """Extract variable/category metadata from a CCHS codebook PDF."""
    from pypdf import PdfReader

    lines = []
    with open(pdf_path, "rb") as file:
        reader = PdfReader(file)
        for page in reader.pages:
            lines.extend(page.extract_text().splitlines())

    return parse_codebook_lines(lines)

Data loading and precomputation

src.data.loader

Data loading functions for CCHS analysis.

build_harmonization_mapping

build_harmonization_mapping(crosswalk, cycle, available_columns)

Build a safe rename mapping for one cycle.

If multiple harmonized variables point at the same source column, prefer the identity mapping (e.g. GEODVHR4 -> GEODVHR4) and otherwise keep the first mapping encountered. This prevents exact-match geography columns from being renamed away by alias entries later in the crosswalk.

Source code in src/data/loader.py
def build_harmonization_mapping(crosswalk: dict, cycle: str, available_columns) -> tuple[dict, list]:
    """
    Build a safe rename mapping for one cycle.

    If multiple harmonized variables point at the same source column, prefer the
    identity mapping (e.g. GEODVHR4 -> GEODVHR4) and otherwise keep the first
    mapping encountered. This prevents exact-match geography columns from being
    renamed away by alias entries later in the crosswalk.
    """
    rename_dict = {}
    available_vars = []
    available_column_set = set(available_columns)

    for harmonized_var, cycle_mapping in crosswalk.items():
        cycle_specific_var = cycle_mapping.get(cycle)
        if not cycle_specific_var or cycle_specific_var == "Not Available":
            continue
        if cycle_specific_var not in available_column_set:
            continue

        existing_target = rename_dict.get(cycle_specific_var)
        if existing_target is None:
            rename_dict[cycle_specific_var] = harmonized_var
            available_vars.append(harmonized_var)
            continue

        if existing_target == cycle_specific_var:
            continue

        if harmonized_var == cycle_specific_var:
            rename_dict[cycle_specific_var] = harmonized_var
            if existing_target in available_vars:
                available_vars.remove(existing_target)
            if harmonized_var not in available_vars:
                available_vars.append(harmonized_var)

    return rename_dict, available_vars

restore_geography_aliases

restore_geography_aliases(df)

Restore expected geography columns when an older harmonization pass renamed them to alias fields.

Source code in src/data/loader.py
def restore_geography_aliases(df: pd.DataFrame) -> pd.DataFrame:
    """
    Restore expected geography columns when an older harmonization pass renamed
    them to alias fields.
    """
    restored = df.copy()

    if 'GEODVHR4' not in restored.columns and 'GEODVOHR' in restored.columns:
        restored['GEODVHR4'] = restored['GEODVOHR']

    return restored

load_cycle_data

load_cycle_data(cycle)

Load main data and bootstrap data for a specific cycle.

Source code in src/data/loader.py
@st.cache_data
def load_cycle_data(cycle: str) -> tuple[pd.DataFrame, pd.DataFrame]:
    """Load main data and bootstrap data for a specific cycle."""
    data_file = os.path.join(DATA_PATH, f"hs{cycle}_on_distr.parquet")
    bootstrap_file = os.path.join(DATA_PATH, f"hs{cycle}_on_bootwt.parquet")

    if os.path.exists(data_file) and os.path.exists(bootstrap_file):
        data = pd.read_parquet(data_file)
        bootstrap_data = pd.read_parquet(bootstrap_file)
        return data, bootstrap_data
    else:
        st.error(f"Data files for cycle {cycle} are missing. Please check the 'data/' directory.")
        return None, None

load_variable_descriptions

load_variable_descriptions(cycle)

Load variable descriptions from harmonized JSON file for a specific cycle.

Source code in src/data/loader.py
@st.cache_data
def load_variable_descriptions(cycle: str) -> tuple[pd.DataFrame, dict]:
    """Load variable descriptions from harmonized JSON file for a specific cycle."""
    try:
        # Load from harmonized JSON file first
        json_file = os.path.join("harmonization", f"CCHS_{cycle}.json")
        if os.path.exists(json_file):
            with open(json_file, "r", encoding="utf-8") as f:
                var_dict = json.load(f)

            # Extract descriptions from JSON structure
            descriptions_data = []
            desc_dict = {}

            for var_name, var_info in var_dict.items():
                description = var_info.get("description", "")
                if description:
                    descriptions_data.append({
                        'Variable': var_name,
                        'Description': description
                    })
                    desc_dict[var_name] = description

            # Ensure all variables are included, even if description is missing
            for var_name in var_dict.keys():
                if var_name not in desc_dict:
                    descriptions_data.append({
                        'Variable': var_name,
                        'Description': "No description available"
                    })
                    desc_dict[var_name] = "No description available"

            # Create DataFrame from extracted data
            if descriptions_data:
                desc_df = pd.DataFrame(descriptions_data)
                return desc_df, desc_dict

        # Fallback to CSV file if JSON doesn't exist or is empty
        desc_file = os.path.join(DATA_PATH, f"CCHS_{cycle}_Recoded_Variables.csv")
        if os.path.exists(desc_file):
            descriptions = pd.read_csv(desc_file)
            desc_dict = dict(zip(descriptions['Variable'], descriptions['Description']))
            return descriptions, desc_dict

        # If neither exists, return empty
        return None, {}

    except Exception as e:
        st.error(f"Error loading variable descriptions for cycle {cycle}: {e}")
        return None, {}

load_json_variable_descriptions

load_json_variable_descriptions(cycle)

Load JSON variable descriptions for a specific cycle (legacy function for compatibility).

Source code in src/data/loader.py
@st.cache_data
def load_json_variable_descriptions(cycle: str) -> dict:
    """Load JSON variable descriptions for a specific cycle (legacy function for compatibility)."""
    json_file = os.path.join("harmonization", f"CCHS_{cycle}.json")
    if os.path.exists(json_file):
        with open(json_file, "r", encoding="utf-8") as f:
            var_dict = json.load(f)
        return {k: v.get("description", "") for k, v in var_dict.items()}
    return {}

load_cycle_variable_info

load_cycle_variable_info(cycle)

Load complete variable information (descriptions AND categories) for a specific cycle.

Source code in src/data/loader.py
@st.cache_data
def load_cycle_variable_info(cycle: str) -> dict:
    """Load complete variable information (descriptions AND categories) for a specific cycle."""
    json_file = os.path.join("harmonization", f"CCHS_{cycle}.json")
    if os.path.exists(json_file):
        with open(json_file, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

load_crosswalk

load_crosswalk()

Load harmonization crosswalk.

Source code in src/data/loader.py
@st.cache_data
def load_crosswalk() -> dict:
    """Load harmonization crosswalk."""
    crosswalk_file = os.path.join("harmonization", "crosswalk.json")
    if os.path.exists(crosswalk_file):
        with open(crosswalk_file, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

load_categories

load_categories()

Load harmonization categories.

Source code in src/data/loader.py
@st.cache_data
def load_categories() -> dict:
    """Load harmonization categories."""
    categories_file = os.path.join("harmonization", "categories.json")
    if os.path.exists(categories_file):
        with open(categories_file, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

load_ontario_csd_lookup

load_ontario_csd_lookup()

Load the Ontario census subdivision code-to-name lookup.

Source code in src/data/loader.py
@st.cache_data
def load_ontario_csd_lookup() -> dict:
    """Load the Ontario census subdivision code-to-name lookup."""
    lookup_file = os.path.join("harmonization", "ontario_csd_lookup.json")
    if os.path.exists(lookup_file):
        with open(lookup_file, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

load_ontario_official_municipalities

load_ontario_official_municipalities()

Load the official Ontario municipalities lookup keyed by CSD code.

Source code in src/data/loader.py
@st.cache_data
def load_ontario_official_municipalities() -> dict:
    """Load the official Ontario municipalities lookup keyed by CSD code."""
    lookup_file = os.path.join("harmonization", "ontario_official_municipalities.json")
    if os.path.exists(lookup_file):
        with open(lookup_file, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

merge_data

merge_data(filtered_data, bootstrap_data)

Merge survey records with bootstrap weights without pooling cycles.

Multi-cycle trend analysis keeps each survey cycle independent. When a CYCLE column is present, it must be present on both frames and becomes part of the join key. The one-to-one validation prevents repeated IDs from silently multiplying records and biasing estimates.

Source code in src/data/loader.py
@st.cache_data
def merge_data(filtered_data, bootstrap_data):
    """Merge survey records with bootstrap weights without pooling cycles.

    Multi-cycle trend analysis keeps each survey cycle independent. When a
    ``CYCLE`` column is present, it must be present on both frames and becomes
    part of the join key. The one-to-one validation prevents repeated IDs from
    silently multiplying records and biasing estimates.
    """
    survey_has_cycle = 'CYCLE' in filtered_data.columns
    bootstrap_has_cycle = 'CYCLE' in bootstrap_data.columns

    if survey_has_cycle != bootstrap_has_cycle:
        raise ValueError(
            "Cycle-aware analysis requires a CYCLE column on both survey and "
            "bootstrap data. Regenerate precomputed files if necessary."
        )

    join_columns = ['ONT_ID']
    if survey_has_cycle:
        join_columns.insert(0, 'CYCLE')

    return pd.merge(
        filtered_data,
        bootstrap_data,
        on=join_columns,
        how='left',
        validate='one_to_one',
    )

load_multi_cycle_data

load_multi_cycle_data(cycles, crosswalk, categories)

Load and harmonize data from multiple cycles using pre-computed crosswalk. This uses simple column renaming (no value transformation) for performance.

Parameters:

Name Type Description Default
cycles list

List of cycle years to load (e.g., ["2021", "2022", "2023"])

required
crosswalk dict

Crosswalk dictionary mapping harmonized_var -> {cycle: cycle_specific_var}

required
categories dict

Categories dictionary (not used, kept for compatibility)

required

Returns:

Name Type Description
frames tuple[DataFrame | None, DataFrame | None]

Combined survey and bootstrap frames, or (None, None) if an error occurs.

Source code in src/data/loader.py
@st.cache_data
def load_multi_cycle_data(cycles: list, crosswalk: dict, categories: dict):
    """
    Load and harmonize data from multiple cycles using pre-computed crosswalk.
    This uses simple column renaming (no value transformation) for performance.

    Args:
        cycles: List of cycle years to load (e.g., ["2021", "2022", "2023"])
        crosswalk: Crosswalk dictionary mapping harmonized_var -> {cycle: cycle_specific_var}
        categories: Categories dictionary (not used, kept for compatibility)

    Returns:
        frames (tuple[pd.DataFrame | None, pd.DataFrame | None]): Combined survey
            and bootstrap frames, or (None, None) if an error occurs.
    """
    if not cycles:
        st.error("No cycles specified for multi-cycle loading.")
        return None, None

    if not crosswalk:
        st.warning("No crosswalk provided. Loading cycles without harmonization.")

    combined_data_list = []
    combined_bootstrap_list = []

    for cycle in cycles:
        # Load raw data for this cycle
        data, bootstrap_data = load_cycle_data(cycle)
        if data is None or bootstrap_data is None:
            st.warning(f"Skipping cycle {cycle} due to missing data files.")
            continue

        rename_dict = {}
        if crosswalk:
            rename_dict, _ = build_harmonization_mapping(crosswalk, cycle, data.columns)

        # Apply harmonization - just column rename, no value transformation (fast!)
        harmonized_data = data.rename(columns=rename_dict) if rename_dict else data.copy()
        harmonized_data = restore_geography_aliases(harmonized_data)
        harmonized_data['CYCLE'] = cycle

        cycle_bootstrap = bootstrap_data.copy()
        cycle_bootstrap['CYCLE'] = cycle

        combined_data_list.append(harmonized_data)
        combined_bootstrap_list.append(cycle_bootstrap)

    if not combined_data_list:
        st.error("No valid cycles could be loaded.")
        return None, None

    # Before the concat line, check for and remove duplicate columns
    for i, df in enumerate(combined_data_list):
        duplicates = df.columns[df.columns.duplicated()].unique()
        if len(duplicates) > 0:
            import streamlit as st
            st.warning(f"Cycle {cycles[i]} has duplicate columns: {duplicates.tolist()}")
            print(f"Duplicate columns in cycle {cycles[i]}: {duplicates.tolist()}")
            # Remove duplicate columns, keeping first occurrence
            combined_data_list[i] = df.loc[:, ~df.columns.duplicated()]

    # Same check for bootstrap data if it exists
    if combined_bootstrap_list:
        for i, df in enumerate(combined_bootstrap_list):
            duplicates = df.columns[df.columns.duplicated()].unique()
            if len(duplicates) > 0:
                import streamlit as st
                st.warning(f"Cycle {cycles[i]} bootstrap data has duplicate columns: {duplicates.tolist()}")
                combined_bootstrap_list[i] = df.loc[:, ~df.columns.duplicated()]

    # Combine all cycles (fast concat operation)
    combined_data = pd.concat(combined_data_list, ignore_index=True)
    combined_bootstrap = pd.concat(combined_bootstrap_list, ignore_index=True)

    return combined_data, combined_bootstrap

load_data

load_data()

Legacy function - loads 2021 data by default for backward compatibility.

Source code in src/data/loader.py
@st.cache_data
def load_data():
    """Legacy function - loads 2021 data by default for backward compatibility."""
    return load_cycle_data("2021")

src.data.precompute

Precompute harmonized datasets for multi-cycle analysis.

check_precompute_status

check_precompute_status(cycles, save_dir=PRECOMPUTE_DIR)

Check which cycles have been precomputed.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years (e.g., ["2021", "2022", "2023"])

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
Dict[str, bool]

Dictionary mapping cycle -> True/False (precomputed status)

Source code in src/data/precompute.py
def check_precompute_status(cycles: List[str], save_dir: Path = PRECOMPUTE_DIR) -> Dict[str, bool]:
    """
    Check which cycles have been precomputed.

    Args:
        cycles: List of cycle years (e.g., ["2021", "2022", "2023"])
        save_dir: Directory where precomputed data is stored

    Returns:
        Dictionary mapping cycle -> True/False (precomputed status)
    """
    status = {}

    for cycle in cycles:
        data_path = save_dir / f"harmonized_data_{cycle}.parquet"
        bootstrap_path = save_dir / f"harmonized_bootstrap_{cycle}.parquet"
        meta_path = save_dir / f"metadata_{cycle}.json"
        status[cycle] = (
            data_path.exists()
            and bootstrap_path.exists()
            and meta_path.exists()
        )

    return status

load_precomputed_data

load_precomputed_data(cycle, save_dir=PRECOMPUTE_DIR)

Load precomputed harmonized data for a cycle.

Parameters:

Name Type Description Default
cycle str

Cycle year (e.g., "2021")

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
DataFrame

DataFrame with harmonized variables

Raises:

Type Description
FileNotFoundError

If precomputed data doesn't exist

Source code in src/data/precompute.py
def load_precomputed_data(cycle: str, save_dir: Path = PRECOMPUTE_DIR) -> pd.DataFrame:
    """
    Load precomputed harmonized data for a cycle.

    Args:
        cycle: Cycle year (e.g., "2021")
        save_dir: Directory where precomputed data is stored

    Returns:
        DataFrame with harmonized variables

    Raises:
        FileNotFoundError: If precomputed data doesn't exist
    """
    path = save_dir / f"harmonized_data_{cycle}.parquet"

    if not path.exists():
        raise FileNotFoundError(f"Precomputed data not found: {path}")

    return pd.read_parquet(path)

load_precomputed_bootstrap

load_precomputed_bootstrap(cycle, save_dir=PRECOMPUTE_DIR)

Load precomputed bootstrap weights for a cycle.

Parameters:

Name Type Description Default
cycle str

Cycle year (e.g., "2021")

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
DataFrame

DataFrame with bootstrap weights (ONT_ID and BSW columns)

Raises:

Type Description
FileNotFoundError

If precomputed data doesn't exist

Source code in src/data/precompute.py
def load_precomputed_bootstrap(cycle: str, save_dir: Path = PRECOMPUTE_DIR) -> pd.DataFrame:
    """
    Load precomputed bootstrap weights for a cycle.

    Args:
        cycle: Cycle year (e.g., "2021")
        save_dir: Directory where precomputed data is stored

    Returns:
        DataFrame with bootstrap weights (ONT_ID and BSW columns)

    Raises:
        FileNotFoundError: If precomputed data doesn't exist
    """
    path = save_dir / f"harmonized_bootstrap_{cycle}.parquet"

    if not path.exists():
        raise FileNotFoundError(f"Precomputed bootstrap data not found: {path}")

    return pd.read_parquet(path)

load_precomputed_metadata

load_precomputed_metadata(cycle, save_dir=PRECOMPUTE_DIR)

Load metadata for precomputed cycle.

Parameters:

Name Type Description Default
cycle str

Cycle year (e.g., "2021")

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
Dict

Dictionary with metadata (available_vars, record_count, etc.)

Raises:

Type Description
FileNotFoundError

If metadata doesn't exist

Source code in src/data/precompute.py
def load_precomputed_metadata(cycle: str, save_dir: Path = PRECOMPUTE_DIR) -> Dict:
    """
    Load metadata for precomputed cycle.

    Args:
        cycle: Cycle year (e.g., "2021")
        save_dir: Directory where precomputed data is stored

    Returns:
        Dictionary with metadata (available_vars, record_count, etc.)

    Raises:
        FileNotFoundError: If metadata doesn't exist
    """
    path = save_dir / f"metadata_{cycle}.json"

    if not path.exists():
        raise FileNotFoundError(f"Metadata not found: {path}")

    with open(path, 'r', encoding='utf-8') as f:
        return json.load(f)

get_common_variables

get_common_variables(cycles, save_dir=PRECOMPUTE_DIR)

Get variables available across all selected cycles using precomputed metadata.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
List[str]

Sorted list of common harmonized variable names

Source code in src/data/precompute.py
def get_common_variables(cycles: List[str], save_dir: Path = PRECOMPUTE_DIR) -> List[str]:
    """
    Get variables available across all selected cycles using precomputed metadata.

    Args:
        cycles: List of cycle years
        save_dir: Directory where precomputed data is stored

    Returns:
        Sorted list of common harmonized variable names
    """
    if not cycles:
        return []

    # Load metadata for each cycle
    all_vars = []
    for cycle in cycles:
        try:
            meta = load_precomputed_metadata(cycle, save_dir)
            all_vars.append(set(meta['available_vars']))
        except FileNotFoundError:
            return []  # Missing precomputed data

    # Find intersection
    common = set.intersection(*all_vars) if all_vars else set()
    return sorted(list(common))

precompute_cycle_data

precompute_cycle_data(cycle, crosswalk, data_path='data', save_dir=PRECOMPUTE_DIR)

Precompute harmonized data for a single cycle.

Creates: - harmonized_data_{cycle}.parquet: Harmonized survey data - harmonized_bootstrap_{cycle}.parquet: Bootstrap weights with ONT_ID - metadata_{cycle}.json: Variable metadata and availability

Category value labels are not baked in here - they're resolved at display time from categories.json via get_value_label().

Parameters:

Name Type Description Default
cycle str

Cycle year (e.g., "2021")

required
crosswalk Dict

Crosswalk dictionary for variable harmonization

required
data_path str

Path to raw data files

'data'
save_dir Path

Directory to save precomputed files

PRECOMPUTE_DIR

Returns:

Type Description
Tuple[DataFrame, DataFrame, Dict]

Tuple of (harmonized_data, harmonized_bootstrap, metadata)

Source code in src/data/precompute.py
@st.cache_data
def precompute_cycle_data(
    cycle: str,
    crosswalk: Dict,
    data_path: str = "data",
    save_dir: Path = PRECOMPUTE_DIR
) -> Tuple[pd.DataFrame, pd.DataFrame, Dict]:
    """
    Precompute harmonized data for a single cycle.

    Creates:
    - harmonized_data_{cycle}.parquet: Harmonized survey data
    - harmonized_bootstrap_{cycle}.parquet: Bootstrap weights with ONT_ID
    - metadata_{cycle}.json: Variable metadata and availability

    Category value labels are not baked in here - they're resolved at
    display time from categories.json via get_value_label().

    Args:
        cycle: Cycle year (e.g., "2021")
        crosswalk: Crosswalk dictionary for variable harmonization
        data_path: Path to raw data files
        save_dir: Directory to save precomputed files

    Returns:
        Tuple of (harmonized_data, harmonized_bootstrap, metadata)
    """
    import os
    print(f"Precomputing {cycle}...")

    # Create output directory if needed
    save_dir.mkdir(parents=True, exist_ok=True)

    # Load raw data
    data_file = os.path.join(data_path, f"hs{cycle}_on_distr.parquet")
    bootstrap_file = os.path.join(data_path, f"hs{cycle}_on_bootwt.parquet")

    if not os.path.exists(data_file) or not os.path.exists(bootstrap_file):
        raise FileNotFoundError(f"Raw data files not found for cycle {cycle}")

    data = pd.read_parquet(data_file)
    bootstrap_data = pd.read_parquet(bootstrap_file)

    # Create harmonization mapping for this cycle
    rename_dict, available_vars = build_harmonization_mapping(
        crosswalk, cycle, data.columns
    )

    # Apply harmonization (rename columns)
    harmonized_data = data.rename(columns=rename_dict)
    harmonized_data = restore_geography_aliases(harmonized_data)

    # Handle duplicate columns (keep first occurrence only)
    # This can happen when multiple cycle-specific variables map to same harmonized name
    if harmonized_data.columns.duplicated().any():
        duplicate_cols = harmonized_data.columns[harmonized_data.columns.duplicated()].unique().tolist()
        print(f"   ⚠️  Warning: Removing {len(duplicate_cols)} duplicate columns: {duplicate_cols[:10]}{'...' if len(duplicate_cols) > 10 else ''}")
        harmonized_data = harmonized_data.loc[:, ~harmonized_data.columns.duplicated(keep='first')]

    # Add CYCLE column for multi-cycle identification
    harmonized_data['CYCLE'] = cycle

    # Keep only harmonized columns plus core variables
    core_vars = ['ONT_ID', 'WTS_S', 'GEODVHR4', 'GEODVCSD']
    # Add age column (varies by cycle)
    if 'DHH_AGE' in harmonized_data.columns:
        core_vars.append('DHH_AGE')
    if 'AWCAGE' in harmonized_data.columns:
        core_vars.append('AWCAGE')

    # Keep harmonized vars + core vars that exist
    cols_to_keep = list(set(available_vars + core_vars + ['CYCLE']) & set(harmonized_data.columns))
    harmonized_data = harmonized_data[cols_to_keep]

    # Cycle-qualified bootstrap data keeps trend comparisons independent and
    # prevents repeated ONT_ID values in different years from cross-joining.
    harmonized_bootstrap = bootstrap_data.copy()
    harmonized_bootstrap['CYCLE'] = cycle

    # Save harmonized data
    output_path = save_dir / f"harmonized_data_{cycle}.parquet"
    harmonized_data.to_parquet(output_path, index=False)

    # Save harmonized bootstrap
    bootstrap_output_path = save_dir / f"harmonized_bootstrap_{cycle}.parquet"
    harmonized_bootstrap.to_parquet(bootstrap_output_path, index=False)

    # Save metadata
    metadata = {
        'cycle': cycle,
        'available_vars': available_vars,
        'record_count': len(harmonized_data),
        'core_vars': core_vars,
        'precompute_date': pd.Timestamp.now().isoformat(),
        'harmonization_mapping': rename_dict
    }

    metadata_path = save_dir / f"metadata_{cycle}.json"
    with open(metadata_path, 'w', encoding='utf-8') as f:
        json.dump(metadata, f, indent=2)

    print(f"✅ {cycle}: {len(available_vars)} variables, {len(harmonized_data):,} records")
    print(f"   Saved to: {output_path}")

    return harmonized_data, harmonized_bootstrap, metadata

precompute_all_cycles

precompute_all_cycles(cycles, crosswalk, data_path='data', save_dir=PRECOMPUTE_DIR)

Precompute data for all specified cycles.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years to precompute

required
crosswalk Dict

Crosswalk dictionary

required
data_path str

Path to raw data files

'data'
save_dir Path

Directory to save precomputed files

PRECOMPUTE_DIR

Returns:

Type Description
Dict[str, Dict]

Dictionary mapping cycle -> {'data': DataFrame, 'bootstrap': DataFrame, 'metadata': Dict}

Source code in src/data/precompute.py
def precompute_all_cycles(
    cycles: List[str],
    crosswalk: Dict,
    data_path: str = "data",
    save_dir: Path = PRECOMPUTE_DIR
) -> Dict[str, Dict]:
    """
    Precompute data for all specified cycles.

    Args:
        cycles: List of cycle years to precompute
        crosswalk: Crosswalk dictionary
        data_path: Path to raw data files
        save_dir: Directory to save precomputed files

    Returns:
        Dictionary mapping cycle -> {'data': DataFrame, 'bootstrap': DataFrame, 'metadata': Dict}
    """
    results = {}

    for cycle in cycles:
        try:
            data, bootstrap, meta = precompute_cycle_data(
                cycle, crosswalk, data_path, save_dir
            )
            results[cycle] = {
                'data': data,
                'bootstrap': bootstrap,
                'metadata': meta
            }
        except Exception as e:
            print(f"❌ Failed to precompute {cycle}: {e}")
            import traceback
            traceback.print_exc()

    return results

run_precompute_workflow

run_precompute_workflow(cycles, crosswalk, data_path='data', save_dir=PRECOMPUTE_DIR)

Run the full precompute workflow and return a compact status summary.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years to precompute

required
crosswalk Dict

Crosswalk dictionary

required
data_path str

Path to raw data files

'data'
save_dir Path

Directory to save precomputed files

PRECOMPUTE_DIR

Returns:

Type Description
Dict

Dictionary with success flag, validation results, and per-cycle summary.

Source code in src/data/precompute.py
def run_precompute_workflow(
    cycles: List[str],
    crosswalk: Dict,
    data_path: str = "data",
    save_dir: Path = PRECOMPUTE_DIR
) -> Dict:
    """
    Run the full precompute workflow and return a compact status summary.

    Args:
        cycles: List of cycle years to precompute
        crosswalk: Crosswalk dictionary
        data_path: Path to raw data files
        save_dir: Directory to save precomputed files

    Returns:
        Dictionary with success flag, validation results, and per-cycle summary.
    """
    results = precompute_all_cycles(
        cycles=cycles,
        crosswalk=crosswalk,
        data_path=data_path,
        save_dir=save_dir,
    )

    if not results:
        return {
            "success": False,
            "results": {},
            "validation": {},
            "message": "No cycles were successfully precomputed.",
        }

    validation = validate_precomputed_data(list(results.keys()), save_dir)

    try:
        create_variable_availability_index(
            cycles=list(results.keys()),
            save_dir=save_dir,
        )
    except Exception as exc:
        print(f"⚠️ Failed to create availability index: {exc}")

    all_valid = all(validation.values()) if validation else False
    return {
        "success": all_valid,
        "results": {
            cycle: {
                "record_count": result["metadata"]["record_count"],
                "available_vars": len(result["metadata"]["available_vars"]),
            }
            for cycle, result in results.items()
        },
        "validation": validation,
        "message": "Precompute completed successfully." if all_valid else "Precompute completed, but validation failed for one or more cycles.",
    }

load_variable_availability_index

load_variable_availability_index(save_dir=PRECOMPUTE_DIR)

Load the variable availability index (which variables exist in which cycles).

Parameters:

Name Type Description Default
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
Dict[str, List[str]]

Dictionary mapping variable_name -> [list of cycles where it exists]

Source code in src/data/precompute.py
def load_variable_availability_index(save_dir: Path = PRECOMPUTE_DIR) -> Dict[str, List[str]]:
    """
    Load the variable availability index (which variables exist in which cycles).

    Args:
        save_dir: Directory where precomputed data is stored

    Returns:
        Dictionary mapping variable_name -> [list of cycles where it exists]
    """
    index_path = save_dir / "variable_availability.json"

    if not index_path.exists():
        raise FileNotFoundError(f"Variable availability index not found: {index_path}")

    with open(index_path, 'r') as f:
        return json.load(f)

create_variable_availability_index

create_variable_availability_index(cycles, save_dir=PRECOMPUTE_DIR)

Create a variable availability index from precomputed metadata.

Parameters:

Name Type Description Default
cycles List[str]

List of cycles to include in index

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
Dict[str, List[str]]

Dictionary mapping variable_name -> [list of cycles where it exists]

Source code in src/data/precompute.py
def create_variable_availability_index(
    cycles: List[str],
    save_dir: Path = PRECOMPUTE_DIR
) -> Dict[str, List[str]]:
    """
    Create a variable availability index from precomputed metadata.

    Args:
        cycles: List of cycles to include in index
        save_dir: Directory where precomputed data is stored

    Returns:
        Dictionary mapping variable_name -> [list of cycles where it exists]
    """
    availability_index = {}

    for cycle in cycles:
        try:
            meta = load_precomputed_metadata(cycle, save_dir)
            for var in meta['available_vars']:
                if var not in availability_index:
                    availability_index[var] = []
                availability_index[var].append(cycle)
        except FileNotFoundError:
            print(f"⚠️ Metadata not found for {cycle}, skipping...")
            continue

    # Save index
    index_path = save_dir / "variable_availability.json"
    with open(index_path, 'w') as f:
        json.dump(availability_index, f, indent=2)

    print(f"✅ Variable availability index created: {len(availability_index)} variables")
    print(f"   Saved to: {index_path}")

    return availability_index

validate_precomputed_data

validate_precomputed_data(cycles, save_dir=PRECOMPUTE_DIR)

Validate that precomputed data exists and is loadable for specified cycles.

Parameters:

Name Type Description Default
cycles List[str]

List of cycles to validate

required
save_dir Path

Directory where precomputed data is stored

PRECOMPUTE_DIR

Returns:

Type Description
Dict[str, bool]

Dictionary mapping cycle -> True (valid) or False (invalid/missing)

Source code in src/data/precompute.py
def validate_precomputed_data(cycles: List[str], save_dir: Path = PRECOMPUTE_DIR) -> Dict[str, bool]:
    """
    Validate that precomputed data exists and is loadable for specified cycles.

    Args:
        cycles: List of cycles to validate
        save_dir: Directory where precomputed data is stored

    Returns:
        Dictionary mapping cycle -> True (valid) or False (invalid/missing)
    """
    validation = {}

    for cycle in cycles:
        try:
            # Try loading data
            data = load_precomputed_data(cycle, save_dir)
            bootstrap = load_precomputed_bootstrap(cycle, save_dir)
            metadata = load_precomputed_metadata(cycle, save_dir)

            # Basic validation checks
            checks = [
                len(data) > 0,
                'CYCLE' in data.columns,
                'ONT_ID' in data.columns,
                len(bootstrap) > 0,
                'ONT_ID' in bootstrap.columns,
                'CYCLE' in bootstrap.columns,
                len(metadata.get('available_vars', [])) > 0,
                metadata.get('cycle') == cycle
            ]

            validation[cycle] = all(checks)

            if validation[cycle]:
                print(f"✅ {cycle}: Valid ({len(data):,} records, {len(metadata['available_vars'])} vars)")
            else:
                print(f"❌ {cycle}: Failed validation checks")

        except Exception as e:
            print(f"❌ {cycle}: {e}")
            validation[cycle] = False

    return validation

src.data.smart_loader

Smart data loader that uses precomputed data when available, falls back to real-time.

smart_load_cycle

smart_load_cycle(cycle, crosswalk=None, use_precompute=True)

Smart loader that uses precomputed data when available, falls back to real-time.

Parameters:

Name Type Description Default
cycle str

Cycle name (e.g., "2021")

required
crosswalk Dict

Crosswalk dictionary (needed for real-time harmonization)

None
use_precompute bool

Whether to attempt using precomputed data

True

Returns:

Type Description
Tuple[DataFrame, DataFrame, Dict, bool]

Tuple of (harmonized_data, bootstrap_data, metadata, is_precomputed)

Source code in src/data/smart_loader.py
def smart_load_cycle(
    cycle: str,
    crosswalk: Dict = None,
    use_precompute: bool = True
) -> Tuple[pd.DataFrame, pd.DataFrame, Dict, bool]:
    """
    Smart loader that uses precomputed data when available, falls back to real-time.

    Args:
        cycle: Cycle name (e.g., "2021")
        crosswalk: Crosswalk dictionary (needed for real-time harmonization)
        use_precompute: Whether to attempt using precomputed data

    Returns:
        Tuple of (harmonized_data, bootstrap_data, metadata, is_precomputed)
    """
    is_precomputed = False

    # Try precomputed first if enabled
    if use_precompute:
        try:
            status = check_precompute_status([cycle])
            if status.get(cycle, False):
                data = load_precomputed_data(cycle)
                data = restore_geography_aliases(data)
                bootstrap = load_precomputed_bootstrap(cycle)
                if 'CYCLE' not in bootstrap.columns:
                    raise ValueError(
                        f"Precomputed bootstrap data for {cycle} is missing CYCLE. "
                        "Regenerate precomputed files."
                    )
                metadata = load_precomputed_metadata(cycle)
                is_precomputed = True
                return data, bootstrap, metadata, is_precomputed
        except Exception as e:
            print(f"⚠️ Failed to load precomputed data for {cycle}: {e}")
            print("Falling back to real-time processing...")

    # Fall back to real-time processing
    if crosswalk is None:
        raise ValueError("Crosswalk required for real-time processing")

    # Import here to avoid circular dependency
    from src.data.loader import load_cycle_data

    # Load raw data
    raw_data, bootstrap_data = load_cycle_data(cycle)

    # Create harmonization mapping
    rename_dict, available_vars = build_harmonization_mapping(
        crosswalk, cycle, raw_data.columns
    )

    # Apply harmonization
    harmonized_data = raw_data.rename(columns=rename_dict)
    harmonized_data = restore_geography_aliases(harmonized_data)

    # Add CYCLE column
    harmonized_data['CYCLE'] = cycle
    bootstrap_data = bootstrap_data.copy()
    bootstrap_data['CYCLE'] = cycle

    # Create metadata
    metadata = {
        'cycle': cycle,
        'available_vars': available_vars,
        'record_count': len(harmonized_data),
        'harmonization_mapping': rename_dict,
        'realtime': True
    }

    return harmonized_data, bootstrap_data, metadata, is_precomputed

smart_load_multiple_cycles

smart_load_multiple_cycles(cycles, crosswalk=None, use_precompute=True)

Load multiple cycles using smart loading.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years to load

required
crosswalk Dict

Crosswalk dictionary (needed for real-time fallback)

None
use_precompute bool

Whether to attempt using precomputed data

True

Returns:

Type Description
Dict[str, Dict]

Dict mapping cycle -> { 'data': DataFrame, 'bootstrap': DataFrame, 'metadata': Dict, 'precomputed': bool

Dict[str, Dict]

}

Source code in src/data/smart_loader.py
def smart_load_multiple_cycles(
    cycles: List[str],
    crosswalk: Dict = None,
    use_precompute: bool = True
) -> Dict[str, Dict]:
    """
    Load multiple cycles using smart loading.

    Args:
        cycles: List of cycle years to load
        crosswalk: Crosswalk dictionary (needed for real-time fallback)
        use_precompute: Whether to attempt using precomputed data

    Returns:
        Dict mapping cycle -> {
            'data': DataFrame,
            'bootstrap': DataFrame,
            'metadata': Dict,
            'precomputed': bool
        }
    """
    results = {}

    for cycle in cycles:
        try:
            data, bootstrap, metadata, is_precomputed = smart_load_cycle(
                cycle, crosswalk, use_precompute
            )
            results[cycle] = {
                'data': data,
                'bootstrap': bootstrap,
                'metadata': metadata,
                'precomputed': is_precomputed
            }
        except Exception as e:
            print(f"❌ Error loading {cycle}: {e}")
            import traceback
            traceback.print_exc()
            continue

    return results

get_common_vars_smart

get_common_vars_smart(cycles, crosswalk=None, use_precompute=True)

Get common variables across cycles using smart approach. Uses precomputed metadata when available for speed.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years

required
crosswalk Dict

Crosswalk dictionary (needed for real-time fallback)

None
use_precompute bool

Whether to attempt using precomputed data

True

Returns:

Type Description
List[str]

Sorted list of common harmonized variable names

Source code in src/data/smart_loader.py
def get_common_vars_smart(
    cycles: List[str],
    crosswalk: Dict = None,
    use_precompute: bool = True
) -> List[str]:
    """
    Get common variables across cycles using smart approach.
    Uses precomputed metadata when available for speed.

    Args:
        cycles: List of cycle years
        crosswalk: Crosswalk dictionary (needed for real-time fallback)
        use_precompute: Whether to attempt using precomputed data

    Returns:
        Sorted list of common harmonized variable names
    """
    # Check if all cycles are precomputed
    if use_precompute:
        status = check_precompute_status(cycles)
        all_precomputed = all(status.get(c, False) for c in cycles)

        if all_precomputed:
            # Fast path - use precomputed metadata
            try:
                return get_common_variables(cycles)
            except Exception as e:
                print(f"⚠️ Failed to get common vars from precomputed: {e}")

    # Slow path - load and check manually
    cycle_data = smart_load_multiple_cycles(cycles, crosswalk, use_precompute)

    if not cycle_data:
        return []

    # Get intersection of available vars
    all_vars = [set(result['metadata']['available_vars']) for result in cycle_data.values()]
    common = set.intersection(*all_vars) if all_vars else set()
    return sorted(list(common))

load_and_combine_cycles_smart

load_and_combine_cycles_smart(cycles, crosswalk=None, use_precompute=True)

Load multiple cycles and combine into single DataFrames. Smart loader that uses precomputed data when available.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years to load and combine

required
crosswalk Dict

Crosswalk dictionary (needed for real-time fallback)

None
use_precompute bool

Whether to attempt using precomputed data

True

Returns:

Type Description
Tuple[DataFrame, DataFrame]

Tuple of (combined_data, combined_bootstrap)

Source code in src/data/smart_loader.py
def load_and_combine_cycles_smart(
    cycles: List[str],
    crosswalk: Dict = None,
    use_precompute: bool = True
) -> Tuple[pd.DataFrame, pd.DataFrame]:
    """
    Load multiple cycles and combine into single DataFrames.
    Smart loader that uses precomputed data when available.

    Args:
        cycles: List of cycle years to load and combine
        crosswalk: Crosswalk dictionary (needed for real-time fallback)
        use_precompute: Whether to attempt using precomputed data

    Returns:
        Tuple of (combined_data, combined_bootstrap)
    """
    # Load all cycles
    cycle_data = smart_load_multiple_cycles(cycles, crosswalk, use_precompute)

    if not cycle_data:
        raise ValueError("No cycles were successfully loaded")

    # Combine data
    combined_data_list = []
    combined_bootstrap_list = []

    for cycle, result in cycle_data.items():
        combined_data_list.append(result['data'])
        combined_bootstrap_list.append(result['bootstrap'])

    # Concatenate
    combined_data = pd.concat(combined_data_list, ignore_index=True)
    combined_bootstrap = pd.concat(combined_bootstrap_list, ignore_index=True)

    return combined_data, combined_bootstrap

get_precompute_summary

get_precompute_summary(cycles)

Get summary of precompute status for cycles.

Parameters:

Name Type Description Default
cycles List[str]

List of cycle years to check

required

Returns:

Type Description
Dict

Dictionary with summary information

Source code in src/data/smart_loader.py
def get_precompute_summary(cycles: List[str]) -> Dict:
    """
    Get summary of precompute status for cycles.

    Args:
        cycles: List of cycle years to check

    Returns:
        Dictionary with summary information
    """
    status = check_precompute_status(cycles)

    precomputed = [c for c, s in status.items() if s]
    missing = [c for c, s in status.items() if not s]

    summary = {
        'total_cycles': len(cycles),
        'precomputed_cycles': precomputed,
        'missing_cycles': missing,
        'all_precomputed': len(missing) == 0,
        'none_precomputed': len(precomputed) == 0,
        'partial_precomputed': 0 < len(precomputed) < len(cycles)
    }

    # Try to get common variables if all precomputed
    if summary['all_precomputed']:
        try:
            common_vars = get_common_variables(cycles)
            summary['common_variables_count'] = len(common_vars)
        except Exception:
            summary['common_variables_count'] = None

    return summary

src.data.processor

Data processing functions for CCHS analysis.

create_age_groups

create_age_groups(df, age_column=None, age_bins=None, age_labels=None)

Create age groups from the age column with flexible bin configuration. Automatically detects the correct age column for each cycle: - 2021: DHH_AGE - 2022/2023: AWCAGE - Multi-cycle harmonized: AWCAGE

Parameters:

Name Type Description Default
df DataFrame

DataFrame containing age data

required
age_column str | None

Specific age column name. If None, auto-detects.

None
age_bins list | None

List of bin edges (e.g., [0, 15, 25, 45, 65, 120])

None
age_labels list | None

List of labels for bins (e.g., ['0-14', '15-24', '25-44', '45-64', '65+'])

None

Returns:

Name Type Description
result DataFrame

DataFrame with AgeGroup column added

Source code in src/data/processor.py
def create_age_groups(df, age_column=None, age_bins=None, age_labels=None):
    """
    Create age groups from the age column with flexible bin configuration.
    Automatically detects the correct age column for each cycle:
    - 2021: DHH_AGE
    - 2022/2023: AWCAGE
    - Multi-cycle harmonized: AWCAGE

    Args:
        df (pd.DataFrame): DataFrame containing age data
        age_column (str | None): Specific age column name. If None, auto-detects.
        age_bins (list | None): List of bin edges (e.g., [0, 15, 25, 45, 65, 120])
        age_labels (list | None): List of labels for bins (e.g., ['0-14', '15-24', '25-44', '45-64', '65+'])

    Returns:
        result (pd.DataFrame): DataFrame with AgeGroup column added
    """
    # Use default bins/labels if not provided
    if age_bins is None:
        from config.settings import DEFAULT_AGE_BINS
        age_bins = DEFAULT_AGE_BINS

    if age_labels is None:
        from config.settings import DEFAULT_AGE_LABELS
        age_labels = DEFAULT_AGE_LABELS

    # Auto-detect age column if not specified
    if age_column is None:
        if 'AWCAGE' in df.columns:
            age_column = 'AWCAGE'
        elif 'DHH_AGE' in df.columns:
            age_column = 'DHH_AGE'
        else:
            print("Warning: No age column found (DHH_AGE or AWCAGE). No age groups created.")
            result_df = df.copy()
            result_df['AgeGroup'] = None
            return result_df

    # Check if specified column exists
    if age_column not in df.columns:
        print(f"Warning: Column '{age_column}' not found. No age groups created.")
        result_df = df.copy()
        result_df['AgeGroup'] = None
        return result_df

    # Validate bins and labels
    if len(age_labels) != len(age_bins) - 1:
        st.error(f"Error: Number of labels ({len(age_labels)}) must be one less than bins ({len(age_bins)})")
        result_df = df.copy()
        result_df['AgeGroup'] = None
        return result_df

    # Create a new DataFrame instead of modifying a copy
    result_df = df.copy()

    # Create age groups using pd.cut for flexible binning
    try:
        result_df['AgeGroup'] = pd.cut(
            result_df[age_column],
            bins=age_bins,
            labels=age_labels,
            right=False,
            include_lowest=True
        )

        print(f"Age groups created using column: {age_column}")
        print(f"Bins: {age_bins}")
        print(f"Labels: {age_labels}")
    except Exception as e:
        st.error(f"Error creating age groups: {str(e)}")
        result_df['AgeGroup'] = None

    return result_df

apply_region_filter

apply_region_filter(data, district_codes=None, health_region_codes=None)

Apply geographic filters using optional district and health region code lists.

Source code in src/data/processor.py
def apply_region_filter(data, district_codes=None, health_region_codes=None):
    """
    Apply geographic filters using optional district and health region code lists.
    """
    filtered = data.copy()
    applied_filters = []

    if health_region_codes and 'GEODVHR4' in filtered.columns:
        health_region_set = {int(code) for code in health_region_codes}
        health_region_values = _normalize_geo_code_series(filtered['GEODVHR4'])
        filtered = filtered[health_region_values.isin(health_region_set)]
        applied_filters.append(
            f"GEODVHR4 in {', '.join(_format_health_region_labels(health_region_set))}"
        )

    if district_codes and 'GEODVCSD' in filtered.columns:
        district_code_set = {int(code) for code in district_codes.keys()}
        district_values = _normalize_geo_code_series(filtered['GEODVCSD'])
        filtered = filtered[district_values.isin(district_code_set)]
        applied_filters.append(
            f"GEODVCSD in {', '.join(_format_district_labels(district_code_set))}"
        )

    if applied_filters:
        st.write(f"Applied geographic filters: {', '.join(applied_filters)}")
    else:
        st.write("No region filter applied; using the entire dataset.")

    return filtered

apply_inclusion_flag_filters

apply_inclusion_flag_filters(data, selected_flags)

Apply inclusion flag filters to the dataset. Works for all cycles (2021, 2022, 2023).

Parameters:

Name Type Description Default
data DataFrame

DataFrame to filter

required
selected_flags dict

Dictionary mapping flag_name -> True/False

required

Returns:

Type Description
DataFrame

Filtered DataFrame

Source code in src/data/processor.py
def apply_inclusion_flag_filters(data: pd.DataFrame, selected_flags: dict) -> pd.DataFrame:
    """
    Apply inclusion flag filters to the dataset.
    Works for all cycles (2021, 2022, 2023).

    Args:
        data: DataFrame to filter
        selected_flags: Dictionary mapping flag_name -> True/False

    Returns:
        Filtered DataFrame
    """
    filtered = data.copy()
    applied_filters = []

    # Apply each selected inclusion flag filter
    for flag, should_filter in selected_flags.items():
        if should_filter and flag in filtered.columns:
            # Filter to only include rows where flag == 1
            initial_count = len(filtered)
            filtered = filtered[filtered[flag] == 1]
            final_count = len(filtered)
            applied_filters.append(f"{flag} ({initial_count:,} → {final_count:,} records)")

    if applied_filters:
        st.write(f"Applied inclusion flag filters: {', '.join(applied_filters)}")
    else:
        st.write("No inclusion flag filters applied")

    return filtered

Bootstrap analysis and quality

src.analysis.bootstrap

Bootstrap analysis functions for CCHS data.

run_bootstrap_analysis_for_all_values

run_bootstrap_analysis_for_all_values(merged_data, variable_col, weight_col=DEFAULT_WEIGHT_COLUMN, standards_cycle=None)

Perform bootstrap analysis using vectorized groupby operations. Computes weighted prevalences and bootstrap variances.

Source code in src/analysis/bootstrap.py
def run_bootstrap_analysis_for_all_values(
    merged_data,
    variable_col,
    weight_col=DEFAULT_WEIGHT_COLUMN,
    standards_cycle=None,
):
    """
    Perform bootstrap analysis using vectorized groupby operations.
    Computes weighted prevalences and bootstrap variances.
    """
    # Identify bootstrap weight columns (those starting with 'BSW')
    bootstrap_cols = [col for col in merged_data.columns if col.startswith(BOOTSTRAP_PREFIX)]

    # Precompute total weights for base and bootstrap replicates
    total_weight_base = merged_data[weight_col].sum()
    total_weights_boot = {col: merged_data[col].sum() for col in bootstrap_cols}

    # Compute weighted sums for the base weight grouped by the selected variable
    base_numerators = merged_data.groupby(variable_col)[weight_col].sum()
    base_prevalence = (base_numerators / total_weight_base) * 100
    unweighted_numerators = merged_data.groupby(variable_col).size()
    unweighted_denominator = merged_data[variable_col].notna().sum()

    # Weighted population for each group (sum of weights)
    weighted_population = base_numerators

    # OPTIMIZED: Compute all bootstrap replicates at once using vectorized operations
    # Group by variable and sum all bootstrap columns simultaneously
    bootstrap_sums = merged_data.groupby(variable_col)[bootstrap_cols].sum()

    # Convert total weights to Series for vectorized division
    total_weights_series = pd.Series(total_weights_boot, name='total_weights')

    # Vectorized calculation: divide each bootstrap sum by its corresponding total weight
    replicate_prevalence_df = bootstrap_sums.div(total_weights_series, axis=1) * 100

    # Compute variance, standard deviation, confidence intervals, etc.
    variance = ((replicate_prevalence_df.sub(base_prevalence, axis=0))**2).mean(axis=1)
    std_dev = np.sqrt(variance)
    ci_lower = base_prevalence - CONFIDENCE_Z * std_dev
    ci_upper = base_prevalence + CONFIDENCE_Z * std_dev
    cv = (std_dev / base_prevalence) * 100

    result_df = pd.DataFrame({
        'Value': base_prevalence.index,
        'Prevalence': base_prevalence.values,
        'Unweighted Numerator': unweighted_numerators.reindex(base_prevalence.index).values,
        'Unweighted Denominator': unweighted_denominator,
        'Weighted Population': weighted_population.values,
        'Variance': variance.values,
        'Standard Deviation': std_dev.values,
        'CI Lower': ci_lower.values,
        'CI Upper': ci_upper.values,
        'CV (%)': cv.values,
        'Error': CONFIDENCE_Z * std_dev.values  # for error bars in plots
    }).reset_index(drop=True)

    if str(standards_cycle) in QUALITY_FLAG_CYCLES:
        result_df = apply_cchs_quality_flags(result_df)

    return result_df

src.analysis.quality

Helpers for applying CCHS 2022+ data quality reporting standards.

calculate_effective_sample_size

calculate_effective_sample_size(prevalence_pct, cv_pct)

Calculate effective sample size for a proportion estimate.

Formula from the CCHS 2022+ standards: (1 - p) / (p * CV^2) where p and CV are expressed as proportions, not percentages.

Source code in src/analysis/quality.py
def calculate_effective_sample_size(prevalence_pct: float, cv_pct: float) -> Optional[float]:
    """
    Calculate effective sample size for a proportion estimate.

    Formula from the CCHS 2022+ standards:
    (1 - p) / (p * CV^2)
    where p and CV are expressed as proportions, not percentages.
    """
    if pd.isna(prevalence_pct) or pd.isna(cv_pct):
        return None

    p_hat = prevalence_pct / 100.0
    cv = cv_pct / 100.0

    if p_hat <= 0 or p_hat >= 1:
        return None

    if cv < 0:
        return None

    if math.isclose(cv, 0.0):
        return math.inf

    denominator = p_hat * (cv ** 2)
    if math.isclose(denominator, 0.0):
        return math.inf

    return (1 - p_hat) / denominator

classify_proportion_release

classify_proportion_release(prevalence_pct, cv_pct, numerator_n, denominator_n, ci_lower=None, ci_upper=None)

Classify a proportion estimate using the CCHS 2022+ A/E/F rules.

Source code in src/analysis/quality.py
def classify_proportion_release(
    prevalence_pct: float,
    cv_pct: float,
    numerator_n: Optional[float],
    denominator_n: Optional[float],
    ci_lower: Optional[float] = None,
    ci_upper: Optional[float] = None,
) -> dict:
    """
    Classify a proportion estimate using the CCHS 2022+ A/E/F rules.
    """
    effective_n = calculate_effective_sample_size(prevalence_pct, cv_pct)

    result = {
        "Release Category": "F",
        "Release Action": "Suppress",
        "Effective Sample Size": effective_n,
        "Release Reason": "Insufficient information to classify",
    }

    if pd.isna(prevalence_pct) or pd.isna(cv_pct):
        result["Release Reason"] = "Missing prevalence or coefficient of variation"
        return result

    if prevalence_pct <= 0 or prevalence_pct >= 100:
        result["Release Reason"] = "Estimates of 0% or 100% should never be released"
        return result

    if ci_lower is not None and ci_upper is not None:
        if not pd.isna(ci_lower) and not pd.isna(ci_upper):
            if math.isclose(ci_lower, ci_upper):
                result["Release Reason"] = "Confidence interval has zero length"
                return result
            if ci_lower < 0 or ci_upper > 100:
                result["Release Reason"] = "Confidence interval bounds are implausible"
                return result

    n1 = float(numerator_n) if numerator_n is not None and not pd.isna(numerator_n) else None
    n2 = float(denominator_n) if denominator_n is not None and not pd.isna(denominator_n) else None

    if n1 is None or n2 is None or effective_n is None:
        result["Release Reason"] = "Missing numerator, denominator, or effective sample size"
        return result

    if n2 < 50:
        result["Release Reason"] = "Denominator unweighted count is below 50"
        return result

    if n1 < 10:
        result["Release Reason"] = "Numerator unweighted count is below 10"
        return result

    if effective_n < 30:
        result["Release Reason"] = "Effective sample size is below 30"
        return result

    if n2 >= 100 and effective_n >= 60:
        result["Release Category"] = "A"
        result["Release Action"] = "Release with no warning"
        result["Release Reason"] = "Denominator >= 100 and effective sample size >= 60"
        return result

    result["Release Category"] = "E"
    result["Release Action"] = "Release with caution warning"
    result["Release Reason"] = "Releasable, but does not meet the no-warning threshold"
    return result

apply_cchs_quality_flags

apply_cchs_quality_flags(result_df)

Append CCHS 2022+ release-quality fields to a result dataframe.

Source code in src/analysis/quality.py
def apply_cchs_quality_flags(result_df: pd.DataFrame) -> pd.DataFrame:
    """Append CCHS 2022+ release-quality fields to a result dataframe."""
    if result_df.empty:
        return result_df

    classified = result_df.apply(
        lambda row: classify_proportion_release(
            prevalence_pct=row.get("Prevalence"),
            cv_pct=row.get("CV (%)"),
            numerator_n=row.get("Unweighted Numerator"),
            denominator_n=row.get("Unweighted Denominator"),
            ci_lower=row.get("CI Lower"),
            ci_upper=row.get("CI Upper"),
        ),
        axis=1,
        result_type="expand",
    )

    return pd.concat([result_df, classified], axis=1)

src.analysis.comparison

Comparative analysis functions for multi-cycle CCHS data.

compare_cycles

compare_cycles(results_df)

Pivot results to show cycles side-by-side for comparison.

Parameters:

Name Type Description Default
results_df DataFrame

DataFrame with columns including 'Variable', 'Value', 'CYCLE', 'Prevalence', etc.

required

Returns:

Type Description
DataFrame

Pivoted DataFrame with cycles as columns

Source code in src/analysis/comparison.py
def compare_cycles(results_df: pd.DataFrame) -> pd.DataFrame:
    """
    Pivot results to show cycles side-by-side for comparison.

    Args:
        results_df: DataFrame with columns including 'Variable', 'Value', 'CYCLE', 'Prevalence', etc.

    Returns:
        Pivoted DataFrame with cycles as columns
    """
    if 'CYCLE' not in results_df.columns:
        return results_df

    pivot_df = results_df.pivot_table(
        index=['Variable', 'Value'],
        columns='CYCLE',
        values='Prevalence',
        aggfunc='first'
    ).reset_index()

    return pivot_df

calculate_change

calculate_change(cycle1_val, cycle2_val)

Calculate percentage point change between two cycles.

Parameters:

Name Type Description Default
cycle1_val float

Prevalence value from first cycle

required
cycle2_val float

Prevalence value from second cycle

required

Returns:

Type Description
float

Percentage point change (cycle2 - cycle1)

Source code in src/analysis/comparison.py
def calculate_change(cycle1_val: float, cycle2_val: float) -> float:
    """
    Calculate percentage point change between two cycles.

    Args:
        cycle1_val: Prevalence value from first cycle
        cycle2_val: Prevalence value from second cycle

    Returns:
        Percentage point change (cycle2 - cycle1)
    """
    if pd.isna(cycle1_val) or pd.isna(cycle2_val):
        return np.nan
    return cycle2_val - cycle1_val

calculate_percent_change

calculate_percent_change(cycle1_val, cycle2_val)

Calculate percentage change between two cycles.

Parameters:

Name Type Description Default
cycle1_val float

Prevalence value from first cycle

required
cycle2_val float

Prevalence value from second cycle

required

Returns:

Type Description
float

Percentage change ((cycle2 - cycle1) / cycle1 * 100)

Source code in src/analysis/comparison.py
def calculate_percent_change(cycle1_val: float, cycle2_val: float) -> float:
    """
    Calculate percentage change between two cycles.

    Args:
        cycle1_val: Prevalence value from first cycle
        cycle2_val: Prevalence value from second cycle

    Returns:
        Percentage change ((cycle2 - cycle1) / cycle1 * 100)
    """
    if pd.isna(cycle1_val) or pd.isna(cycle2_val) or cycle1_val == 0:
        return np.nan
    return ((cycle2_val - cycle1_val) / cycle1_val) * 100

calculate_trend

calculate_trend(results_df)

Calculate trend direction (increasing/decreasing/stable) across cycles.

Parameters:

Name Type Description Default
results_df DataFrame

DataFrame with 'CYCLE', 'Variable', 'Value', 'Prevalence' columns

required

Returns:

Type Description
DataFrame

DataFrame with added 'Trend' column indicating direction

Source code in src/analysis/comparison.py
def calculate_trend(results_df: pd.DataFrame) -> pd.DataFrame:
    """
    Calculate trend direction (increasing/decreasing/stable) across cycles.

    Args:
        results_df: DataFrame with 'CYCLE', 'Variable', 'Value', 'Prevalence' columns

    Returns:
        DataFrame with added 'Trend' column indicating direction
    """
    if 'CYCLE' not in results_df.columns:
        return results_df

    result_df = results_df.copy()
    result_df['Trend'] = None

    for (variable, value), group in result_df.groupby(['Variable', 'Value']):
        if len(group) < 2:
            continue

        sorted_group = group.sort_values('CYCLE')
        prevalences = sorted_group['Prevalence'].values

        if len(prevalences) == 2:
            if prevalences[1] > prevalences[0] * 1.05:
                trend = 'Increasing'
            elif prevalences[1] < prevalences[0] * 0.95:
                trend = 'Decreasing'
            else:
                trend = 'Stable'
        else:
            slope = np.polyfit(range(len(prevalences)), prevalences, 1)[0]
            if slope > 0.1:
                trend = 'Increasing'
            elif slope < -0.1:
                trend = 'Decreasing'
            else:
                trend = 'Stable'

        result_df.loc[group.index, 'Trend'] = trend

    return result_df

test_significance

test_significance(cycle1_results, cycle2_results, variable, value=None)

Test statistical significance of difference between two cycles.

Uses overlapping confidence intervals as a simple test. More sophisticated tests could be added later.

Parameters:

Name Type Description Default
cycle1_results DataFrame

Results DataFrame for first cycle

required
cycle2_results DataFrame

Results DataFrame for second cycle

required
variable str

Variable name to test

required
value Optional[str]

Optional specific value to test

None

Returns:

Type Description
dict

Dictionary with test results including 'significant' boolean

Source code in src/analysis/comparison.py
def test_significance(cycle1_results: pd.DataFrame, cycle2_results: pd.DataFrame, 
                      variable: str, value: Optional[str] = None) -> dict:
    """
    Test statistical significance of difference between two cycles.

    Uses overlapping confidence intervals as a simple test.
    More sophisticated tests could be added later.

    Args:
        cycle1_results: Results DataFrame for first cycle
        cycle2_results: Results DataFrame for second cycle
        variable: Variable name to test
        value: Optional specific value to test

    Returns:
        Dictionary with test results including 'significant' boolean
    """
    filter1 = cycle1_results['Variable'] == variable
    filter2 = cycle2_results['Variable'] == variable

    if value is not None:
        filter1 = filter1 & (cycle1_results['Value'] == value)
        filter2 = filter2 & (cycle2_results['Value'] == value)

    result1 = cycle1_results[filter1].iloc[0] if len(cycle1_results[filter1]) > 0 else None
    result2 = cycle2_results[filter2].iloc[0] if len(cycle2_results[filter2]) > 0 else None

    if result1 is None or result2 is None:
        return {'significant': False, 'reason': 'Missing data'}

    ci1_lower = result1.get('CI Lower', np.nan)
    ci1_upper = result1.get('CI Upper', np.nan)
    ci2_lower = result2.get('CI Lower', np.nan)
    ci2_upper = result2.get('CI Upper', np.nan)

    if pd.isna(ci1_lower) or pd.isna(ci1_upper) or pd.isna(ci2_lower) or pd.isna(ci2_upper):
        return {'significant': False, 'reason': 'Missing confidence intervals'}

    overlap = not (ci1_upper < ci2_lower or ci2_upper < ci1_lower)

    return {
        'significant': not overlap,
        'overlap': overlap,
        'cycle1_ci': (ci1_lower, ci1_upper),
        'cycle2_ci': (ci2_lower, ci2_upper),
        'cycle1_prevalence': result1.get('Prevalence', np.nan),
        'cycle2_prevalence': result2.get('Prevalence', np.nan)
    }

create_comparison_summary

create_comparison_summary(results_df)

Create a summary table comparing cycles.

Parameters:

Name Type Description Default
results_df DataFrame

DataFrame with cycle comparison results

required

Returns:

Type Description
DataFrame

Summary DataFrame with comparison statistics

Source code in src/analysis/comparison.py
def create_comparison_summary(results_df: pd.DataFrame) -> pd.DataFrame:
    """
    Create a summary table comparing cycles.

    Args:
        results_df: DataFrame with cycle comparison results

    Returns:
        Summary DataFrame with comparison statistics
    """
    if 'CYCLE' not in results_df.columns:
        return pd.DataFrame()

    summary_data = []

    for (variable, value), group in results_df.groupby(['Variable', 'Value']):
        if len(group) < 2:
            continue

        sorted_group = group.sort_values('CYCLE')
        cycles = sorted_group['CYCLE'].tolist()
        prevalences = sorted_group['Prevalence'].tolist()

        first_cycle = cycles[0]
        last_cycle = cycles[-1]
        first_prev = prevalences[0]
        last_prev = prevalences[-1]

        change_pp = calculate_change(first_prev, last_prev)
        change_pct = calculate_percent_change(first_prev, last_prev)

        trend_df = calculate_trend(group)
        trend = trend_df['Trend'].iloc[0] if 'Trend' in trend_df.columns else None

        summary_data.append({
            'Variable': variable,
            'Value': value,
            'First Cycle': first_cycle,
            'Last Cycle': last_cycle,
            'First Prevalence': first_prev,
            'Last Prevalence': last_prev,
            'Change (pp)': change_pp,
            'Change (%)': change_pct,
            'Trend': trend
        })

    return pd.DataFrame(summary_data)

Helpers

src.utils.helpers

Utility helper functions for the CCHS application.

format_number

format_number(num, format_type='comma')

Format numbers with different styles.

Source code in src/utils/helpers.py
def format_number(num, format_type="comma"):
    """Format numbers with different styles."""
    if format_type == "comma":
        return f"{num:,}"
    elif format_type == "percentage":
        return f"{num:.2f}%"
    elif format_type == "decimal":
        return f"{num:.3f}"
    else:
        return str(num)

create_excel_download

create_excel_download(data, sheet_name='Analysis Results')

Create Excel file in memory for download.

Source code in src/utils/helpers.py
def create_excel_download(data: pd.DataFrame, sheet_name: str = "Analysis Results") -> bytes:
    """Create Excel file in memory for download."""
    excel_buffer = BytesIO()
    with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer:
        data.to_excel(writer, sheet_name=sheet_name, index=False)
    return excel_buffer.getvalue()

validate_data_columns

validate_data_columns(data, required_columns)

Validate that required columns exist in the dataset.

Source code in src/utils/helpers.py
def validate_data_columns(data: pd.DataFrame, required_columns: list) -> bool:
    """Validate that required columns exist in the dataset."""
    missing_columns = [col for col in required_columns if col not in data.columns]
    if missing_columns:
        st.error(f"Missing required columns: {', '.join(missing_columns)}")
        return False
    return True

safe_division

safe_division(numerator, denominator, default=0)

Safely divide two numbers, returning default if denominator is zero.

Source code in src/utils/helpers.py
def safe_division(numerator, denominator, default=0):
    """Safely divide two numbers, returning default if denominator is zero."""
    try:
        return numerator / denominator if denominator != 0 else default
    except (TypeError, ZeroDivisionError):
        return default

filter_dataframe_by_values

filter_dataframe_by_values(df, column, values)

Filter dataframe by specific values in a column.

Source code in src/utils/helpers.py
def filter_dataframe_by_values(df, column, values):
    """Filter dataframe by specific values in a column."""
    if column in df.columns and values:
        return df[df[column].isin(values)]
    return df

get_memory_usage_mb

get_memory_usage_mb(df)

Get memory usage of dataframe in MB.

Source code in src/utils/helpers.py
def get_memory_usage_mb(df):
    """Get memory usage of dataframe in MB."""
    return df.memory_usage(deep=True).sum() / 1024**2

get_cycle_varname

get_cycle_varname(harmonized_var, cycle, crosswalk)

Helper to get cycle-specific variable name from harmonization crosswalk.

Source code in src/utils/helpers.py
def get_cycle_varname(harmonized_var: str, cycle: str, crosswalk: dict) -> str:
    """Helper to get cycle-specific variable name from harmonization crosswalk."""
    mapping = crosswalk.get(harmonized_var, {})
    return mapping.get(cycle, harmonized_var)

get_value_label

get_value_label(harmonized_var, value, cycle, categories)

Helper to get value label for cycle from harmonization categories.

Source code in src/utils/helpers.py
def get_value_label(harmonized_var: str, value, cycle: str, categories: dict) -> str:
    """Helper to get value label for cycle from harmonization categories."""
    cat = categories.get(harmonized_var, {})
    mappings = cat.get("mappings", {})
    year_map = mappings.get(str(cycle), {})

    # Convert value to string, but if it's a float and is_integer, cast to int first
    if isinstance(value, float) and value.is_integer():
        value_str = str(int(value))
    else:
        value_str = str(value)

    label = year_map.get(value_str, None)
    if label is None:
        return value_str
    return label

get_cycle_value_label

get_cycle_value_label(varname, value, cycle_var_info)

Get value label from cycle-specific JSON (CCHS_YYYY.json). Used for single-cycle analysis to show proper category labels.

Parameters:

Name Type Description Default
varname str

Variable name (cycle-specific, not harmonized)

required
value object

The value to get label for

required
cycle_var_info dict

Full cycle variable info from CCHS_YYYY.json

required

Returns:

Type Description
str

Label string or original value if not found

Source code in src/utils/helpers.py
def get_cycle_value_label(varname: str, value, cycle_var_info: dict) -> str:
    """
    Get value label from cycle-specific JSON (CCHS_YYYY.json).
    Used for single-cycle analysis to show proper category labels.

    Args:
        varname: Variable name (cycle-specific, not harmonized)
        value (object): The value to get label for
        cycle_var_info: Full cycle variable info from CCHS_YYYY.json

    Returns:
        Label string or original value if not found
    """
    var_info = cycle_var_info.get(varname, {})
    categories = var_info.get("categories", {})

    # Convert value to string (handle floats that are actually integers)
    if isinstance(value, float) and value.is_integer():
        value_str = str(int(value))
    else:
        value_str = str(value)

    # Return label if found, otherwise return the value as string
    return categories.get(value_str, value_str)

get_available_harmonized_vars

get_available_harmonized_vars(crosswalk, cycle, merged_data)

Helper to get available harmonized variables for the selected cycle and data.

Source code in src/utils/helpers.py
def get_available_harmonized_vars(crosswalk: dict, cycle: str, merged_data: pd.DataFrame) -> list:
    """Helper to get available harmonized variables for the selected cycle and data."""
    available = []
    for harmonized_var, mapping in crosswalk.items():
        varname = mapping.get(cycle)
        if varname and varname in merged_data.columns:
            available.append(harmonized_var)
    return available

merge_descriptions

merge_descriptions(json_desc_dict, csv_desc_dict)

Merge CSV and JSON descriptions, preferring CSV if present.

Source code in src/utils/helpers.py
def merge_descriptions(json_desc_dict: dict, csv_desc_dict: dict) -> dict:
    """Merge CSV and JSON descriptions, preferring CSV if present."""
    return {**json_desc_dict, **csv_desc_dict}

create_multi_cycle_excel

create_multi_cycle_excel(results_df, cycles)

Create Excel file with multiple sheets for multi-cycle results.

Parameters:

Name Type Description Default
results_df DataFrame

DataFrame with multi-cycle results (must contain 'CYCLE' column)

required
cycles list

List of cycles included in the results

required

Returns:

Type Description
bytes

Excel file as bytes

Source code in src/utils/helpers.py
def create_multi_cycle_excel(results_df: pd.DataFrame, cycles: list) -> bytes:
    """
    Create Excel file with multiple sheets for multi-cycle results.

    Args:
        results_df: DataFrame with multi-cycle results (must contain 'CYCLE' column)
        cycles: List of cycles included in the results

    Returns:
        Excel file as bytes
    """
    excel_buffer = BytesIO()

    with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer:
        results_df.to_excel(writer, sheet_name="Combined Results", index=False)

        for cycle in cycles:
            cycle_data = results_df[results_df['CYCLE'] == cycle].copy()
            if not cycle_data.empty:
                cycle_data = cycle_data.drop(columns=['CYCLE'])
                cycle_data.to_excel(writer, sheet_name=f"Cycle {cycle}", index=False)

        from src.analysis.comparison import create_comparison_summary
        summary_df = create_comparison_summary(results_df)
        if not summary_df.empty:
            summary_df.to_excel(writer, sheet_name="Comparison Summary", index=False)

    return excel_buffer.getvalue()

get_inclusion_flags

get_inclusion_flags(data, desc_dict)

Detect inclusion flag variables in the dataset. Inclusion flags are identified by having "Inclusion Flag" in their description. Works for all cycles (2021, 2022, 2023).

Parameters:

Name Type Description Default
data DataFrame

DataFrame with columns

required
desc_dict dict

Dictionary mapping variable names to descriptions (required)

required

Returns:

Type Description
dict

Dictionary mapping flag_name -> description

Source code in src/utils/helpers.py
def get_inclusion_flags(data: pd.DataFrame, desc_dict: dict) -> dict:
    """
    Detect inclusion flag variables in the dataset.
    Inclusion flags are identified by having "Inclusion Flag" in their description.
    Works for all cycles (2021, 2022, 2023).

    Args:
        data: DataFrame with columns
        desc_dict: Dictionary mapping variable names to descriptions (required)

    Returns:
        Dictionary mapping flag_name -> description
    """
    inclusion_flags = {}

    if desc_dict is None or not desc_dict:
        return inclusion_flags

    # Check all variables in the dataset
    for col in data.columns:
        # Skip bootstrap weights and other non-variable columns
        if col.startswith('BSW') or col in ['ONT_ID', 'CYCLE', 'WTS_S', 'AgeGroup']:
            continue

        # Check if description contains "Inclusion Flag"
        if col in desc_dict:
            desc = desc_dict[col]
            if 'Inclusion Flag' in desc or 'inclusion flag' in desc.lower():
                inclusion_flags[col] = desc

    return inclusion_flags