Source code for bhf_smarthealth.parse.qualtrics_parser

"""This module contains functions to process raw Qualtrics data ingressed to the Smart Health pipeline"""

import numpy as np
import pandas as pd
import logging

from typing import Any
from pydantic import ValidationError
from pathlib import Path

# from bhf_smarthealth.validate.qualtrics_schema import FitbitSummaryData
from bhf_smarthealth.utils import data_io
from bhf_smarthealth.utils import identifiers
from bhf_smarthealth.validate.qualtrics_schema import QualtricsStreamSchema
from bhf_smarthealth.exceptions import EmptyFileError, DataValidationError

logger = logging.getLogger(__name__)


[docs] def load_qualtrics_csv(file_path: Path | str) -> list[dict[str, Any]]: """ Load the raw CSV file, and convert it to a flat dictionary. This function assumes row is the header, and skips rows 1 and 2 that are multi-index rows output from the Qualtrics platform. Parameters ---------- file_path : str | Path Path to the data file to be processed. Returns ------- dict[str, Any] A dictionary containing the raw, unvalidated Qualtrics data, with the extracted file metadata (sheffield_id and date) appended """ # Load raw file and raise a custom exception if the file is completely empty try: data = data_io.load_data_from_file(file_path, header=0, skiprows=[1, 2]) except pd.errors.EmptyDataError: raise EmptyFileError(f"No usable data in file {file_path}") # Also want to raise an "empty file error" if only the headers exist if len(data) == 0: raise EmptyFileError(f"No usable data in file {file_path}") data = data.replace({np.nan: None}) # convert dataframe to dictionary of rows data = data.to_dict(orient="records") return data
[docs] def validate_data(base_questions_data, bridge_data) -> QualtricsStreamSchema: """Uses Pydantic model to convert and validate the raw Qualtrics data. Stream is validated together to ensure all recieved IDs exist within each of the component files. Parameters ---------- base_questions_data : list[dict[str, Any]] List of dictionaries containing one row of the raw base questions data bridge_data : list[dict[str, Any]] List of dictionaries containing one row of the raw bridge file data Returns ------- QualtricsStreamSchema Pydantic model class containing the validated data Raises ------ DataValidationError Raised if the pydantic schema validation fails """ try: return QualtricsStreamSchema( base_questions=base_questions_data, bridge_file=bridge_data ) 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_dataframes(validated_data: QualtricsStreamSchema) -> dict[str, pd.DataFrame]: """Creates the "base_questions" and "bridge_file" dataframes from the validated Pydantic model, output as a dictionary. Includes derivation of the deterministic global_id for the bridge_file. Parameters ---------- validated_data : QualtricsStreamSchema Pydantic model class containing the validated data Returns ------- dict[str, pd.DataFrame] Dictionary containing the "base_questions" and "bridge_file" dataframe """ validated_dict = validated_data.model_dump() base_df = pd.DataFrame(validated_dict["base_questions"]).set_index("sheffield_id") bridge_df = pd.DataFrame(validated_dict["bridge_file"]).set_index("sheffield_id") # deterministically generate the global_id using the sheffield_id index to ensure idempotency bridge_df["global_id"] = bridge_df.index.map( lambda x: identifiers.generate_deterministic_uuid_from_args(x) ) return {"base": base_df, "bridge": bridge_df}