Skip to content

Commit 8a20533

Browse files
authored
Merge pull request #155 from cmu-delphi/fb-package-validation
FB-survey validation with a generic design to include other pipelines
2 parents 0046ca7 + e2c86a3 commit 8a20533

19 files changed

+6355
-0
lines changed

validator/PLANS.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Validator checks and features
2+
3+
## Current checks for indicator source data
4+
5+
* Missing dates within the selected range
6+
* Recognized file name format
7+
* Recognized geographical type (county, state, etc)
8+
* Recognized geo id format (e.g. state is two lowercase letters)
9+
* Specific geo id has been seen before, in historical data
10+
* Missing geo type + signal + date combos based on the geo type + signal combos Covidcast metadata says should be available
11+
* Missing ‘val’ values
12+
* Negative ‘val’ values
13+
* Out-of-range ‘val’ values (>0 for all signals, <=100 for percents, <=100 000 for proportions)
14+
* Missing ‘se’ values
15+
* Appropriate ‘se’ values, within a calculated reasonable range
16+
* Stderr != 0
17+
* If signal and stderr both = 0 (seen in Quidel data due to lack of Jeffreys correction, [issue 255](https://github.com/cmu-delphi/covidcast-indicators/issues/255#issuecomment-692196541))
18+
* Missing ‘sample_size’ values
19+
* Appropriate ‘sample_size’ values, ≥ 100 (default) or user-defined threshold
20+
* Most recent date seen in source data is recent enough, < 1 day ago (default) or user-defined on a per-signal basis
21+
* Most recent date seen in source data is not in the future
22+
* Most recent date seen in source data is not older than most recent date seen in reference data
23+
* Similar number of obs per day as recent API data (static threshold)
24+
* Similar average value as API data (static threshold)
25+
* Source data for specified date range is empty
26+
* API data for specified date range is empty
27+
28+
29+
## Current features
30+
31+
* Errors and warnings are summarized in class attribute and printed on exit
32+
* If any non-suppressed errors are raised, the validation process exits with non-zero status
33+
* Various check settings are controllable via indicator-specific params.json files
34+
* User can manually disable specific checks for specific datasets using a field in the params.json file
35+
* User can enable test mode (checks only a small number of data files) using a field in the params.json file
36+
37+
## Checks + features wishlist, and problems to think about
38+
39+
### Starter/small issues
40+
41+
* Check for duplicate rows
42+
* Backfill problems, especially with JHU and USA Facts, where a change to old data results in a datapoint that doesn’t agree with surrounding data ([JHU examples](https://delphi-org.slack.com/archives/CF9G83ZJ9/p1600729151013900)) or is very different from the value it replaced. If date is already in the API, have any values changed significantly within the "backfill" window (use span_length setting). See [this](https://github.com/cmu-delphi/covidcast-indicators/pull/155#discussion_r504195207) for context.
43+
* Run check_missing_date_files (or similar) on every geo type-signal type separately in comparative checks loop.
44+
45+
### Larger issues
46+
47+
* Expand framework to support nchs_mortality, which is provided on a weekly basis and has some differences from the daily data. E.g. filenames use a different format ("weekly_YYYYWW_geotype_signalname.csv")
48+
* Make backtesting framework so new checks can be run individually on historical indicator data to tune false positives, output verbosity, understand frequency of error raising, etc. Should pull data from API the first time and save locally in `cache` dir.
49+
* Add DETAILS.md doc with detailed descriptions of what each check does and how. Will be especially important for statistical/anomaly detection checks.
50+
* Improve errors and error report
51+
* Check if [errors raised from validating all signals](https://docs.google.com/spreadsheets/d/1_aRBDrNeaI-3ZwuvkRNSZuZ2wfHJk6Bxj35Ol_XZ9yQ/edit#gid=1226266834) are correct, not false positives, not overly verbose or repetitive
52+
* Easier suppression of many errors at once
53+
* Maybe store errors as dict of dicts. Keys could be check strings (e.g. "check_bad_se"), then next layer geo type, etc
54+
* Nicer formatting for error “report”.
55+
* E.g. if a single type of error is raised for many different datasets, summarize all error messages into a single message? But it still has to be clear how to suppress each individually
56+
* Check for erratic data sources that wrongly report all zeroes
57+
* E.g. the error with the Wisconsin data for the 10/26 forecasts
58+
* Wary of a purely static check for this
59+
* Are there any geo regions where this might cause false positives? E.g. small counties or MSAs, certain signals (deaths, since it's << cases)
60+
* This test is partially captured by checking avgs in source vs reference data, unless erroneous zeroes continue for more than a week
61+
* Also partially captured by outlier checking. If zeroes aren't outliers, then it's hard to say that they're erroneous at all.
62+
* Outlier detection (in progress)
63+
* Current approach is tuned to daily cases and daily deaths; use just on those signals?
64+
* prophet (package) detection is flexible, but needs 2-3 months historical data to fit on. May make sense to use if other statistical checks also need that much data.
65+
* Use known erroneous/anomalous days of source data to tune static thresholds and test behavior
66+
* If can't get data from API, do we want to use substitute data for the comparative checks instead?
67+
* E.g. most recent successful API pull -- might end up being a couple weeks older
68+
* Currently, any API fetch problems just doesn't do comparative checks at all.
69+
* Improve performance and reduce runtime (no particular goal, just avoid being painfully slow!)
70+
* Profiling (iterate)
71+
* Save intermediate files?
72+
* Currently a bottleneck at "individual file checks" section. Parallelize?
73+
* Make `all_frames` MultiIndex-ed by geo type and signal name? Make a dict of data indexed by geo type and signal name? May improve performance or may just make access more readable.
74+
* Ensure validator runs on signals that require AWS credentials (iterate)
75+
76+
### Longer-term issues
77+
78+
* Data correctness and consistency over longer time periods (weeks to months). Compare data against long-ago (3 months?) API data for changes in trends.
79+
* Long-term trends and correlations between time series. Currently, checks only look at a data window of a few days
80+
* Any relevant anomaly detection packages already exist?
81+
* What sorts of hypothesis tests to use? See [time series trend analysis](https://www.genasis.cz/time-series/index.php?pg=home--trend-analysis).
82+
* See data-quality GitHub issues, Ryan’s [correlation notebook](https://github.com/cmu-delphi/covidcast/tree/main/R-notebooks), and Dmitry's [indicator validation notebook](https://github.com/cmu-delphi/covidcast-indicators/blob/deploy-jhu/testing_utils/indicator_validation.template.ipynb) for ideas
83+
* E.g. Doctor visits decreasing correlation with cases
84+
* E.g. WY/RI missing or very low compared to historical
85+
* Use hypothesis testing p-values to decide when to raise error or not, instead of static thresholds. Many low but non-significant p-values will also raise error. See [here](https://delphi-org.slack.com/archives/CV1SYBC90/p1601307675021000?thread_ts=1600277030.103500&cid=CV1SYBC90) and [here](https://delphi-org.slack.com/archives/CV1SYBC90/p1600978037007500?thread_ts=1600277030.103500&cid=CV1SYBC90) for more background.
86+
* Order raised exceptions by p-value
87+
* Raise errors when one p-value (per geo region, e.g.) is significant OR when a bunch of p-values for that same type of test (different geo regions, e.g.) are "close" to significant
88+
* Correct p-values for multiple testing
89+
* Bonferroni would be easy but is sensitive to choice of "family" of tests; Benjamimi-Hochberg is a bit more involved but is less sensitive to choice of "family"; [comparison of the two](https://delphi-org.slack.com/archives/D01A9KNTPKL/p1603294915000500)

validator/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Validator
2+
3+
The validator performs two main tasks:
4+
1) Sanity checks on daily data generated from the pipeline of a specific data
5+
source.
6+
2) Comparative analysis with recent data from the API
7+
to detect any anomalies, such as spikes or significant value differences
8+
9+
The validator validates new source data in CSV format against data pulled from the [COVIDcast API](https://cmu-delphi.github.io/delphi-epidata/api/covidcast.html).
10+
11+
12+
## Running the Validator
13+
14+
The validator is run by executing the Python module contained in this
15+
directory from the main directory of the indicator of interest.
16+
17+
The safest way to do this is to create a virtual environment,
18+
install the common DELPHI tools, install the indicator module and its
19+
dependencies, and then install the validator module and its
20+
dependencies to the virtual environment.
21+
22+
To do this, navigate to the main directory of the indicator of interest and run the following code:
23+
24+
```
25+
python -m venv env
26+
source env/bin/activate
27+
pip install ../_delphi_utils_python/.
28+
pip install .
29+
pip install ../validator
30+
```
31+
32+
To execute the module and validate source data (by default, in `receiving`), run the indicator to generate data files, then run
33+
the validator, as follows:
34+
35+
```
36+
env/bin/python -m delphi_INDICATORNAME
37+
env/bin/python -m delphi_validator
38+
```
39+
40+
Once you are finished with the code, you can deactivate the virtual environment
41+
and (optionally) remove the environment itself.
42+
43+
```
44+
deactivate
45+
rm -r env
46+
```
47+
48+
### Customization
49+
50+
All of the user-changable parameters are stored in the `validation` field of the indicator's `params.json` file. If `params.json` does not already include a `validation` field, please copy that provided in this module's `params.json.template`.
51+
52+
Please update the follow settings:
53+
54+
* `data_source`: should match the [formatting](https://cmu-delphi.github.io/delphi-epidata/api/covidcast_signals.html) as used in COVIDcast API calls
55+
* `end_date`: specifies the last date to be checked; if set to "latest", `end_date` will always be the current date
56+
* `span_length`: specifies the number of days before the `end_date` to check. `span_length` should be long enough to contain all recent source data that is still in the process of being updated (i.e. in the backfill period), for example, if the data source of interest has a 2-week lag before all reports are in for a given date, `scan_length` should be 14 days
57+
* `smoothed_signals`: list of the names of the signals that are smoothed (e.g. 7-day average)
58+
* `expected_lag`: dictionary of signal name-int pairs specifying the number of days of expected lag (time between event occurrence and when data about that event was published) for that signal
59+
* `test_mode`: boolean; `true` checks only a small number of data files
60+
* `suppressed_errors`: list of lists uniquely specifying errors that have been manually verified as false positives or acceptable deviations from expected
61+
62+
All other fields contain working defaults, to be modified as needed.
63+
64+
## Testing the code
65+
66+
To test the code, please create a new virtual environment in the main module directory using the following procedure, similar to above:
67+
68+
```
69+
python -m venv env
70+
source env/bin/activate
71+
pip install ../_delphi_utils_python/.
72+
pip install .
73+
```
74+
75+
To do a static test of the code style, it is recommended to run **pylint** on
76+
the module. To do this, run the following from the main module directory:
77+
78+
```
79+
env/bin/pylint delphi_validator
80+
```
81+
82+
The most aggressive checks are turned off; only relatively important issues
83+
should be raised and they should be manually checked (or better, fixed).
84+
85+
Unit tests are also included in the module. To execute these, run the following command from this directory:
86+
87+
```
88+
(cd tests && ../env/bin/pytest --cov=delphi_validator --cov-report=term-missing)
89+
```
90+
91+
The output will show the number of unit tests that passed and failed, along with the percentage of code covered by the tests. None of the tests should fail and the code lines that are not covered by unit tests should be small and should not include critical sub-routines.
92+
93+
94+
## Code tour
95+
96+
* run.py: sends params.json fields to and runs the validation process
97+
* datafetcher.py: methods for loading source and API data
98+
* validate.py: methods for validating data. Includes the individual check methods and supporting functions.
99+
* errors.py: custom errors
100+
101+
102+
## Adding checks
103+
104+
To add a new validation check, define the check as a `Validator` class method in `validate.py`. Each check should append a descriptive error message to the `raised` attribute if triggered. All checks should allow the user to override exception raising for a specific file using the `suppressed_errors` setting in `params.json`.
105+
106+
This features requires that the `check_data_id` defined for an error uniquely identifies that combination of check and test data. This usually takes the form of a tuple of strings with the check method and test identifier, and test data filename or date, geo type, and signal name.
107+
108+
Add the newly defined check to the `validate()` method to be executed. It should go in one of three sections:
109+
110+
* data sanity checks where a data file is compared against static format settings,
111+
* data trend and value checks where a set of source data (can be one or several days) is compared against recent API data, from the previous few days,
112+
* data trend and value checks where a set of source data is compared against long term API data, from the last few months

validator/REVIEW.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## Code Review (Python)
2+
3+
A code review of this module should include a careful look at the code and the
4+
output. To assist in the process, but certainly not in replace of it, please
5+
check the following items.
6+
7+
**Documentation**
8+
9+
- [ ] the README.md file template is filled out and currently accurate; it is
10+
possible to load and test the code using only the instructions given
11+
- [ ] minimal docstrings (one line describing what the function does) are
12+
included for all functions; full docstrings describing the inputs and expected
13+
outputs should be given for non-trivial functions
14+
15+
**Structure**
16+
17+
- [ ] code should use 4 spaces for indentation; other style decisions are
18+
flexible, but be consistent within a module
19+
- [ ] any required metadata files are checked into the repository and placed
20+
within the directory `static`
21+
- [ ] any intermediate files that are created and stored by the module should
22+
be placed in the directory `cache`
23+
- [ ] all options and API keys are passed through the file `params.json`
24+
- [ ] template parameter file (`params.json.template`) is checked into the
25+
code; no personal (i.e., usernames) or private (i.e., API keys) information is
26+
included in this template file
27+
28+
**Testing**
29+
30+
- [ ] module can be installed in a new virtual environment
31+
- [ ] pylint with the default `.pylint` settings run over the module produces
32+
minimal warnings; warnings that do exist have been confirmed as false positives
33+
- [ ] reasonably high level of unit test coverage covering all of the main logic
34+
of the code (e.g., missing coverage for raised errors that do not currently seem
35+
possible to reach are okay; missing coverage for options that will be needed are
36+
not)
37+
- [ ] all unit tests run without errors
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# -*- coding: utf-8 -*-
2+
"""Module to validate indicator source data before uploading to the public COVIDcast API.
3+
4+
This file defines the functions that are made public by the module. As the
5+
module is intended to be executed though the main method, these are primarily
6+
for testing.
7+
"""
8+
9+
from __future__ import absolute_import
10+
11+
from . import run
12+
13+
__version__ = "0.1.0"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
"""Call the function run_module when executed.
3+
4+
This file indicates that running the module (`python -m delphi_validator`) will
5+
call the function `run_module` found within the run.py file.
6+
"""
7+
8+
from .run import run_module
9+
10+
run_module()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Functions to get CSV filenames and data.
4+
"""
5+
6+
import re
7+
from os import listdir
8+
from os.path import isfile, join
9+
from itertools import product
10+
import pandas as pd
11+
import numpy as np
12+
13+
import covidcast
14+
from .errors import APIDataFetchError
15+
16+
filename_regex = re.compile(
17+
r'^(?P<date>\d{8})_(?P<geo_type>\w+?)_(?P<signal>\w+)\.csv$')
18+
19+
20+
def read_filenames(path):
21+
"""
22+
Return a list of tuples of every filename and regex match to the CSV filename
23+
format in the specified directory.
24+
25+
Arguments:
26+
- path: path to the directory containing CSV data files.
27+
28+
Returns:
29+
- list of tuples
30+
"""
31+
daily_filenames = [(f, filename_regex.match(f))
32+
for f in listdir(path) if isfile(join(path, f))]
33+
return daily_filenames
34+
35+
36+
def load_csv(path):
37+
"""
38+
Load CSV with specified column types.
39+
"""
40+
return pd.read_csv(
41+
path,
42+
dtype={
43+
'geo_id': str,
44+
'val': float,
45+
'se': float,
46+
'sample_size': float,
47+
})
48+
49+
50+
def get_geo_signal_combos(data_source):
51+
"""
52+
Get list of geo type-signal type combinations that we expect to see, based on
53+
combinations reported available by COVIDcast metadata.
54+
"""
55+
meta = covidcast.metadata()
56+
source_meta = meta[meta['data_source'] == data_source]
57+
unique_signals = source_meta['signal'].unique().tolist()
58+
unique_geotypes = source_meta['geo_type'].unique().tolist()
59+
60+
geo_signal_combos = list(product(unique_geotypes, unique_signals))
61+
print("Number of expected geo region-signal combinations:",
62+
len(geo_signal_combos))
63+
64+
return geo_signal_combos
65+
66+
67+
def fetch_api_reference(data_source, start_date, end_date, geo_type, signal_type):
68+
"""
69+
Get and process API data for use as a reference. Formatting is changed
70+
to match that of source data CSVs.
71+
"""
72+
api_df = covidcast.signal(
73+
data_source, signal_type, start_date, end_date, geo_type)
74+
75+
if not isinstance(api_df, pd.DataFrame):
76+
custom_msg = "Error fetching data from " + str(start_date) + \
77+
" to " + str(end_date) + \
78+
"for data source: " + data_source + \
79+
", signal type: " + signal_type + \
80+
", geo type: " + geo_type
81+
82+
raise APIDataFetchError(custom_msg)
83+
84+
column_names = ["geo_id", "val",
85+
"se", "sample_size", "time_value"]
86+
87+
# Replace None with NA to make numerical manipulation easier.
88+
# Rename and reorder columns to match those in df_to_test.
89+
api_df = api_df.replace(
90+
to_replace=[None], value=np.nan
91+
).rename(
92+
columns={'geo_value': "geo_id", 'stderr': 'se', 'value': 'val'}
93+
).drop(
94+
['direction', 'issue', 'lag'], axis=1
95+
).reindex(columns=column_names)
96+
97+
return api_df

0 commit comments

Comments
 (0)