Quality Control
QualityControl (AeroViz/rawDataReader/core/qc.py) holds the statistical
filters the readers' QC rules are built from, and QCRule / QCFlagBuilder
turn a list of rules into the QC_Flag / QC_Invalid columns.
How rules, severities, the verdict and the summary fit together is explained once, in RawDataReader Reference §2; the per-instrument rule lists live on each instrument page. This page covers calling the filters directly on your own data.
Declaring a rule
from AeroViz.rawDataReader.core import QCRule
QCRule(
name='Invalid BC',
condition=lambda df: (df['BC6'] <= 0) | (df['BC6'] > 20000), # True where the row FAILS
description='BC outside 0-20000 ng/m³',
# severity='error' is the default: the row is masked to NaN in the output
)
QCRule(
name='Below MDL',
condition=lambda df: df['Thermal_OC'] <= 0.3,
description='At or below the method detection limit',
severity='warning', # advisory: recorded and logged, value kept
)
QCFlagBuilder.apply(df) evaluates every rule as a vectorised mask and writes
QC_Flag (every rule that fired, comma-joined, or "Valid") and QC_Invalid
(True iff an 'error' rule fired). get_summary(df) reports count and
percentage per rule plus the Valid and Usable totals. Reclassify a rule per
run with RawDataReader(..., flag_severity={'Insufficient': 'error'}).
Filters
Statistical outliers
cleaned = qc.n_sigma(df, std_range=3) # N standard deviations
cleaned = qc.iqr(df) # interquartile range
cleaned = qc.iqr(df, log_dist=True) # IQR on log-transformed data
cleaned = qc.time_aware_rolling_iqr(df, window_size='24h', iqr_factor=3.0, min_periods=5)
Trend-aware outliers
outlier_mask = qc.bidirectional_trend_std_QC(
df, window_size='6h', std_factor=3.0, trend_window='30min', trend_factor=2.0)
final_df = df.where(~outlier_mask, np.nan)
Spikes
spike_mask = qc.spike_detection(df['PM_Total']) # change > 3 × median |Δ|, or a sharp up-down reversal
Instrument status
filter_error_status supports four status_type modes — bitwise (AE33,
AE43, BC1054, MA350, TEOM), numeric (Aurora, NEPH), text (SMPS) and
binary_string (APS). ignored_values is the whitelist that RawDataReader
exposes as ignored_status_errors, interpreted in the active mode; see
status modes.
# bitwise: drop one warning bit from the error definition
# (e.g. ignore the TEOM "Dryer A" status bit 0x20000000)
error_mask = qc.filter_error_status(
df, error_codes=[1 << b for b in range(32)],
status_column='status', status_type='bitwise',
ignored_values=[536870912],
)
# bitwise with special codes (AE33-style)
error_mask = qc.filter_error_status(
df, error_codes=[1, 2, 4, 16, 32], special_codes=[1024, 2048, 4096])
# text: whitelist comma-split tokens (SMPS)
error_mask = qc.filter_error_status(
df, status_column='Instrument Errors', status_type='text',
ok_value='', ignored_values=['Low aerosol flow', 'Neutralizer not active'],
)
qc_df = df.where(~error_mask, np.nan)
Completeness
# fraction of the points an hour could hold at the given frequency
completeness_mask = qc.hourly_completeness_QC(df, freq='6min', threshold=0.75)
API
AeroViz.rawDataReader.core.qc.QualityControl
A class providing various methods for data quality control and outlier detection
Methods:
_ensure_dataframe
staticmethod
Ensure input data is in DataFrame format
_transform_if_log
staticmethod
Transform data to log scale if required
n_sigma
classmethod
Detect outliers using n-sigma method
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data |
required |
std_range
|
int
|
Number of standard deviations to use as threshold |
5
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Cleaned DataFrame with outliers masked as NaN |
iqr
classmethod
Detect outliers using Interquartile Range (IQR) method
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data |
required |
log_dist
|
bool
|
Whether to apply log transformation to data |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Cleaned DataFrame with outliers masked as NaN |
time_aware_rolling_iqr
classmethod
time_aware_rolling_iqr(df: DataFrame, window_size: str = '24h', log_dist: bool = False, iqr_factor: float = 5, min_periods: int = 5) -> DataFrame
Detect outliers using rolling time-aware IQR method with handling for initial periods
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data |
required |
window_size
|
str
|
Size of the rolling window |
'24h'
|
log_dist
|
bool
|
Whether to apply log transformation to data |
False
|
iqr_factor
|
float
|
The factor by which to multiply the IQR |
3
|
min_periods
|
int
|
Minimum number of observations required in window |
4
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Cleaned DataFrame with outliers masked as NaN |
time_aware_std_QC
time_aware_std_QC(df: DataFrame, time_window: str = '6h', std_factor: float = 3.0, min_periods: int = 4) -> DataFrame
Time-aware outlier detection using rolling standard deviation
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data |
required |
time_window
|
str
|
Rolling window size |
'6h'
|
std_factor
|
float
|
Standard deviation multiplier (e.g., 3 means 3σ) |
3.0
|
min_periods
|
int
|
Minimum number of observations required in window |
4
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Quality controlled DataFrame with outliers marked as NaN |
bidirectional_trend_std_QC
classmethod
bidirectional_trend_std_QC(df: DataFrame, window_size: str = '6h', std_factor: float = 3.0, trend_window: str = '30min', trend_factor: float = 2, min_periods: int = 4) -> Series
Perform quality control using standard deviation with awareness of both upward and downward trends.
This method identifies outliers considering both upward and downward trends in the data, applying more lenient criteria when consistent trends are detected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data frame with time series (QC_Flag column is now optional) |
required |
window_size
|
str
|
Size of the rolling window for std calculation |
'6h'
|
std_factor
|
float
|
Base factor for standard deviation threshold |
3.0
|
trend_window
|
str
|
Window for trend detection |
'30min'
|
trend_factor
|
float
|
Factor to increase std_factor when trends are detected |
2
|
min_periods
|
int
|
Minimum number of observations in window |
4
|
Returns:
| Type | Description |
|---|---|
Series
|
Boolean mask where True indicates outliers |
filter_error_status
staticmethod
filter_error_status(_df, error_codes=None, special_codes=None, return_mask=True, status_column='Status', status_type='bitwise', ok_value=None, ignored_values=None)
Filter data based on error status codes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
_df
|
DataFrame
|
Input DataFrame |
required |
error_codes
|
list or array - like
|
Codes indicating errors (for 'bitwise' type) |
None
|
special_codes
|
list or array - like
|
Special codes to handle differently (exact match) |
None
|
return_mask
|
bool
|
If True, returns a boolean mask where True indicates errors; If False, returns filtered DataFrame |
True
|
status_column
|
str
|
Name of the status column in DataFrame |
'Status'
|
status_type
|
str
|
Type of status check: - 'bitwise': Use bitwise AND to check error codes (AE33, AE43, BC1054, MA350) - 'numeric': Check if status != ok_value (TEOM, Aurora, NEPH) - 'text': Check if status != ok_value as string (SMPS) - 'binary_string': Parse binary string and check if > 0 (APS) |
'bitwise'
|
ok_value
|
any
|
The value indicating OK status (for 'numeric', 'text' types) - For 'numeric': typically 0 - For 'text': typically 'Normal Scan' |
None
|
ignored_values
|
list
|
Whitelist of statuses to suppress (treat as OK) without editing the raw files — e.g. an operator-known benign warning. Interpretation is mode-specific; entries that don't fit a mode are silently skipped so a whitelist meant for one instrument is harmless if it reaches another. Defaults to None (no whitelist; behaviour unchanged).
|
None
|
Returns:
| Type | Description |
|---|---|
Union[DataFrame, Series]
|
If return_mask=True: boolean Series with True for error points If return_mask=False: Filtered DataFrame with error points masked |
spike_detection
classmethod
spike_detection(df: DataFrame, max_change_rate: float = 3.0, min_abs_change: float = None) -> Series
Vectorized spike detection using change rate analysis.
Detects sudden unreasonable value changes while allowing legitimate gradual changes during events (pollution episodes, etc.).
This method is much faster than rolling window methods because it uses pure numpy vectorized operations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data frame with time series |
required |
max_change_rate
|
float
|
Maximum allowed ratio of current change to median absolute change. Higher values = more permissive. A value of 3.0 means a change must be 3x larger than the median change to be flagged. |
3.0
|
min_abs_change
|
float
|
Minimum absolute change required to be considered a spike. If None, uses 10% of the data's standard deviation. |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Boolean mask where True indicates detected spikes |
Notes
The algorithm: 1. Calculate absolute difference between consecutive points 2. Calculate the median absolute change (robust baseline) 3. Flag points where change > max_change_rate * median_change 4. Also detect "reversals" (spike up then immediately down)
This approach allows gradual changes during events while catching sudden spikes that are likely instrument errors.
Examples:
hourly_completeness_QC
classmethod
Check whether each clock hour holds enough data to be representative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Input data frame with time series |
required |
freq
|
str
|
Data frequency (e.g. '6min') |
required |
threshold
|
float
|
Minimum required proportion of the points that hour could hold (0-1) |
0.5
|
Returns:
| Type | Description |
|---|---|
Series
|
Boolean mask where True indicates insufficient data |
Notes
This is a statement about representativeness, not validity: the
readings in a sparse hour are perfectly good measurements, it is an
average over that hour that would misrepresent it. Readers therefore
raise it at severity='warning' — see QCRule.
The expectation is scaled by how much of each hour the data actually spans, which matters at the two ends of every read. An hour is compared against the points it could have held given the coverage, not against a full hour it never had the chance to fill: a read starting at 10:54 has six minutes in the 10 o'clock hour, so a full-hour expectation condemns it no matter how perfectly the instrument ran. That is what made short reads unusable — a 22-minute file had every row flagged — while leaving multi-day reads almost untouched, since the effect is always exactly two hours out of however many.
Interior hours are unaffected: they overlap the coverage completely, so their expectation is the full hour and a genuine outage is still caught.
Related
- AbstractReader — the base class that calls these filters
- RawDataReader — the factory with
qc=,flag_severity=,ignored_status_errors=