Source code for bhf_smarthealth.process.healthapp_process

"""This module utilises functions from the parse.healthapp_parser module to batch process raw Health App metrics data"""

import logging
import pandas as pd

from pathlib import Path
from pandas import DataFrame

from bhf_smarthealth.log import metadata_logs
from bhf_smarthealth.parse import healthapp_parser as bhf_ha
from bhf_smarthealth.utils import df_utils, data_io
from bhf_smarthealth.exceptions import (
    RawDataProcessingError,
    EmptyFileError,
)
from bhf_smarthealth.models import DataStreamNames


logger = logging.getLogger(__name__)


[docs] def process_file(part_path: Path) -> dict[str, DataFrame]: """Process an individual raw Health App .json file Parameters ---------- part_path : Path Path to subdirectory containing raw Health App .json file to be processed Returns ------- dict[str, DataFrame] Dictionary containing "workouts" and "metrics" DataFrames Raises ------ FileNotFoundError Raised if a healthapp/*_healthdata.json file cannot be located in this subdirectory """ # returns None if no file matches data_path = next(part_path.glob("healthapp/*_healthdata.json"), None) if not data_path: raise FileNotFoundError( f"No file matching the glob pattern 'healthapp/*_healthdata.json' could be found in the directory {part_path}" ) logger.info(f"Processing {data_path}") data_dict = bhf_ha.load_json(data_path) parsed_data = bhf_ha.validate_data(data_dict) data_dicts = bhf_ha.create_dataframes(parsed_data) logger.info(f"Successfully processed {part_path}") for name in ["workouts", "metrics"]: logger.debug( f"Processed {part_path}. Final metrics dataframe shape: {data_dicts[name].shape}. Dataframe columns: {data_dicts[name].columns} with index: {data_dicts[name].index.names}" ) return data_dicts
[docs] def process_day(in_dir: Path, out_paths: dict[str, Path], date_str: str): """Process a daily batch of raw healthapp .json files Parameters ---------- in_dir : Path Path of the directory containing raw data to batch process out_paths : Path Dictionary containing Paths to output the merged "workouts" and "metrics" DataFrames to date_str: String of date being processed """ df_list = [] quality_counter = metadata_logs.DataQualityCounter() part_paths = [ part_path for part_path in sorted(in_dir.iterdir()) if part_path.is_dir() ] for part_path in part_paths: try: processed_dfs = process_file(part_path) # base exception which catches all raw data validation issues except EmptyFileError: logger.warning(f"File {part_path} is empty. Skipping file..") quality_counter["empty"] += 1 continue except RawDataProcessingError: logger.exception( f"Error with raw data file at {part_path}. Skipping file.." ) quality_counter["processing_error"] += 1 continue except FileNotFoundError: logger.error(f"File missing in {part_path}. Skipping file..") quality_counter["missing"] += 1 continue else: df_list.append(processed_dfs) quality_counter["valid"] += 1 # check whether enough valid data was processed metadata_logs.evaluate_daily_processing_success(counter=quality_counter) # back stop for DEFAULT_VALID_DATA_THRESHOLD being set to 0 if quality_counter["valid"] == 0: df_list.append({"metrics": pd.DataFrame(), "workouts": pd.DataFrame()}) valid_IDs_set = set() # Combine the DataFrames from each participant for both the Health App Metrics and Workouts for table in ["workouts", "metrics"]: # collect the DataFrames comprising one of the output tables frames_per_table = [processed_file[table] for processed_file in df_list] # merge the DataFrames from each participant into a single, daily DataFrame full_df = df_utils.merge_dataframes(frames_per_table) # It is possible that no data was recorded for one of the tables so we should continue to process the next table. if full_df.empty: logger.warning( f"No valid {table} data processed from {in_dir}. Writing empty file.." ) data_io.save_dataframe_ouput(full_df, out_paths[table]) continue data_io.save_dataframe_ouput(full_df, out_paths[table]) logger.info( f"Merged DataFrame saved to {out_paths[table]} with shape: {full_df.shape}, columns: {full_df.columns}, and index: {full_df.index.names}" ) # add the valid IDs seen to the set of valid IDs valid_IDs_set.update(full_df.reset_index()["sheffield_id"].unique().tolist()) all_IDs = [str(part_path.stem) for part_path in part_paths] valid_IDs = list(valid_IDs_set) metadata_logs.generate_daily_IDs_receipt( all_IDs=all_IDs, valid_IDs=valid_IDs, data_stream=DataStreamNames.HEALTHAPP, date_str=date_str, out_path=out_paths["metadata"], )