Source code for bhf_smarthealth.parse.intraday_parser
"""This module contains functions to process raw Fitbit intraday activity data ingressed to the Smart Health pipeline"""
import pandas as pd
import logging
from pathlib import Path
from pydantic import ValidationError
from typing import List, Dict, Any
from bhf_smarthealth.validate.fitbit_schema import IntradayDataList
from bhf_smarthealth.utils import data_io
from bhf_smarthealth.validate import metadata_validation
from bhf_smarthealth.exceptions import DataValidationError, EmptyFileError
logger = logging.getLogger(__name__)
# Load raw json
[docs]
def load_json(json_path: str | Path, activity_source: str) -> List[Dict[str, Any]]:
"""Load the raw JSON file and return list of dictionaries for validation.
Parameters
----------
json_path : str | Path
JSON file Path
activity_source : str
The source of intraday activity data to load (e.g., heart rate or steps).
The key is constructed as "activities-{activity_source}-intraday".
Returns
-------
List[Dict[str, Any]]
List of dictionaries containing raw, unvalidated Fitbit activity datapoints with sheffield_id and date appended
Raises
------
FileNotFoundError
Raised if file not found
ValueError
Raised if file metadata cannot be validated
"""
# Load raw JSON data
data = data_io.load_data_from_file(json_path)
# The next line should be conditioned on the Activity source (HR or steps, extracted from file name)
# Construct the full name for the time series data
activity_source_full = f"activities-{activity_source}-intraday"
time_series = data[activity_source_full]["dataset"]
if not time_series:
raise EmptyFileError(f"No data to process in file {json_path}")
# Extract a list of dates from the datapoints
datetimes = [datapoint["d"] for datapoint in time_series if datapoint.get("d")]
# Extract and validate the metadata in the filename
metadata = metadata_validation.process_metadata(json_path, dates=datetimes)
# Append the validated metadata to the raw data
time_series_with_metadata = [
{**datapoint, "sheffield_id": metadata.sheffield_id}
for datapoint in time_series
]
logger.info(
f"Loaded {len(time_series_with_metadata)} intraday {activity_source} data points from {json_path}"
)
return time_series_with_metadata
[docs]
def validate_data(intraday_data_dict: List[Dict[str, Any]]) -> IntradayDataList:
"""Uses Pydantic model to convert and validate the dictionaries
containing the raw data.
Parameters
----------
intraday_data_dict : List of intraday_datapoint
List of dictionaries containing raw activity datapoints
Returns
-------
IntradayDataList
Pydantic model class containing the converted data
Raises
------
DataValidationError
Raised if the pydantic schema validation fails
"""
try:
# Use model_validate for best practice when loading a dictionary
return IntradayDataList.model_validate(intraday_data_dict)
except ValidationError as e:
# raised ValidationError is re-raised as a custom error that inherits from the RawDataProcessingError exception class
raise DataValidationError(f"Schema validation failed: {e}") from e
[docs]
def create_dataframe(time_series: IntradayDataList) -> pd.DataFrame:
"""Creates a pandas DataFrame from a Pydantic model instance.
Parameters
----------
time_series : IntradayDataList
The Pydantic model instance containing the time-series data,
which is expected to have a 'timestamp' field.
Returns
-------
pd.DataFrame
A DataFrame containing the data from the Pydantic model, with the
'timestamp' field converted to datetime objects and set as the index.
"""
intraday_df = pd.DataFrame(time_series.model_dump())
intraday_df["timestamp"] = pd.to_datetime(
intraday_df["timestamp"], format="%Y-%m-%dT%H:%M:%S", errors="coerce"
) # Invalid datetimes are coerced to NaTs
invalid_dates = intraday_df["timestamp"].isna()
if invalid_dates.any():
logger.warning(f"Found {invalid_dates.sum()} invalid datetime values")
intraday_df.set_index(
"timestamp",
inplace=True,
)
return intraday_df