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
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
|
|
required
|
crosswalk
|
dict
|
|
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
|
Parsing logic for Statistics Canada CCHS Data Dictionary/Freqs PDF codebooks.
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(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 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 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 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:
| Type |
Description |
|
|
Tuple of (combined_data, combined_bootstrap_data) or (None, None) if error
|
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:
Tuple of (combined_data, combined_bootstrap_data) or (None, None) if error
"""
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
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(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]
|
|
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
|
|
'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
|
|
required
|
data_path
|
str
|
|
'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
|
|
required
|
data_path
|
str
|
|
'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]
|
|
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 containing age data
|
required
|
age_column
|
|
Optional specific age column name. If None, auto-detects.
|
None
|
age_bins
|
|
List of bin edges (e.g., [0, 15, 25, 45, 65, 120])
|
None
|
age_labels
|
|
List of labels for bins (e.g., ['0-14', '15-24', '25-44', '45-64', '65+'])
|
None
|
Returns:
| Type |
Description |
|
|
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: DataFrame containing age data
age_column: Optional specific age column name. If None, auto-detects.
age_bins: List of bin edges (e.g., [0, 15, 25, 45, 65, 120])
age_labels: List of labels for bins (e.g., ['0-14', '15-24', '25-44', '45-64', '65+'])
Returns:
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
|
|
required
|
selected_flags
|
dict
|
Dictionary mapping flag_name -> True/False
|
required
|
Returns:
| Type |
Description |
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
|
|
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(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 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
|
|
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: 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:
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
|
|
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
|