Source code for bhf_smarthealth.log.metadata_logs

"""This module contains functions to generate metadata logs."""

import logging
import pandas as pd

from datetime import date
from pathlib import Path
from collections import Counter

from bhf_smarthealth.models import DataStreamNames
from bhf_smarthealth.utils import data_io
from bhf_smarthealth.config import DEFAULT_VALID_DATA_THRESHOLD
from bhf_smarthealth.exceptions import InsufficientValidDataError

logger = logging.getLogger(__name__)


# TODO: Write unit tests for this module


[docs] class DataQualityCounter(Counter[str]): """Custom collections.Counter instance to accept only allowed data quality categories as keys""" categories = ["missing", "empty", "valid", "processing_error"] def __setitem__(self, key: str, value: int) -> None: if key.lower() not in self.categories: raise KeyError( f"Invalid data quality category: {key}. Category should be one of: {', '.join(self.categories)}" ) return super().__setitem__(key, value)
[docs] def evaluate_daily_processing_success( counter: DataQualityCounter, validity_threshold: float = DEFAULT_VALID_DATA_THRESHOLD, ): """Evaluate the data quality in a daily data batch. Logs information about the number of successfully-processed files if the specified threshold is satisfied, else an error is raised. Parameters ---------- counter : DataQualityCounter custom collections.Counter instance that accepts only allowed data quality category keys validity_threshold : float, optional Ratio of invalid : total files allowed to pass through the pipeline, by default DEFAULT_VALID_DATA_THRESHOLD Raises ------ InsufficientValidDataError Raised if an insufficient number of daily data files were successfully processed """ total_files = counter.total() total_invalid = total_files - counter["valid"] if total_files == counter["valid"]: logger.info(f"Successfully processed all {total_files} files") elif counter["valid"] / total_files < validity_threshold: raise InsufficientValidDataError( f"The number of invalid files ({total_invalid}) out of {total_files} total files, corresponding to a data validity ratio of {round(counter['valid'] / total_files, 2)}, does not meet the specified threshold for data validity ({validity_threshold}).\nThe breakdown of invalid files is: {counter}" ) else: logger.warning( f"Only {counter['valid']} files were successfully processed out of {total_files}, corresponding to a data validity ratio of {round(counter['valid'] / total_files, 2)}" )
[docs] def generate_daily_IDs_receipt( all_IDs: list[str], valid_IDs: list[str], data_stream: DataStreamNames, date_str: str, out_path: Path, ) -> None: """Generate a .parquet receipt containing all of the sheffield_ids recieved on a single given date the specified data_type, and whether the data was valid on that date. Parameters ---------- all_IDs : list[str] Full list of strings of IDs for which data has been recieved in a given day valid_IDs : list[str] List of strings of IDs for which valid data has been successfully processed in a given day data_stream : DataStreamNames Data stream name date_str : str Date string passed through snakemake 'date' wildcard in form %Y%m%d out_path : Path Path to save .parquet file to """ date_obj = date.fromisoformat(date_str) df = pd.DataFrame( {"sheffield_id": all_IDs, "data_stream": data_stream, "event_date": date_obj} ) df["is_valid"] = df["sheffield_id"].apply( lambda x: True if x in valid_IDs else False ) data_io.save_dataframe_ouput(df=df, file_out_path=out_path)