Skip to content

CDCWonderNatality : Automation - #2233

Open
kartik-s21 wants to merge 62 commits into
datacommonsorg:masterfrom
kartik-s21:cdcwondernatality-automation
Open

kartik-s21 wants to merge 62 commits into
datacommonsorg:masterfrom
kartik-s21:cdcwondernatality-automation

Conversation

@kartik-s21

@kartik-s21 kartik-s21 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR automates the ingestion pipeline for CDCWonderNatality, migrating the import to the automated Cloud Batch framework and aligning with Google3 CL 986267877.

Bug & CL References

Changes

  1. Manifest Configuration (manifest.json):
    • Configured import_inputs across Country, State, and County levels pointing to distinct template paths (output/country.tmcf, output/state.tmcf, output/county.tmcf) to prevent GCS basename overwrite collisions.
    • Configured annual cron schedule 0 0 1 6 *.
  2. Processing Pipeline (process.py):
    • Merges input CSV files chronologically (_extract_start_year) across reporting revisions (95-02, 03-06, 07-20, 16-22).
    • Uses os.path.basename(f).lower() for safe file classification.
    • Enforces strict regex matching for Geo column (^geoId/\d{2}$ for state vs ^geoId/\d{5}$ for county).
    • Casts aggregated country count Quantity to nullable integer Int64.
    • Copies distinct TMCF templates to output/.
  3. Download Script (download.sh):
    • Downloads latest preprocessed artifacts from GCS (gs://unresolved_mcf/cdc/wonder/natality/).
    • Removes silent error suppression and verifies file presence across all 3 geographic resolutions.
  4. Unit Tests:
    • Added end-to-end unit tests in process_test.py and preprocess_test.py.

Test Execution & Validation Artifacts

Geographic Level Row Count GenMCF Report Summary CSV Differ Summary Validation Output
Country (input0) 1,347 report.json summary_report.csv differ_summary.json validation_output.csv (PASSED)
State (input1) 332,447 report.json summary_report.csv differ_summary.json validation_output.csv (PASSED)
County (input2) 3,069,562 report.json summary_report.csv differ_summary.json validation_output.csv (PASSED)

…nd pipeline scripts

- Implement automated download script download.sh from GCS repository
- Implement unified process.py pipeline for country, state, and county tables
- Add unit tests in process_test.py and ensure deterministic sorting in preprocess.py
- Add manifest.json for Cloud Run/Batch automation (annual cadence, 3 import inputs, no node_mcf)
- Add validation_config.json with empty import, lint error, and missing refs checks
- Update README.md with comprehensive documentation and usage instructions
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@kartik-s21

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an automated end-to-end data processing pipeline for CDC Wonder Natality data, including new scripts for downloading, processing, and testing, along with a manifest and validation configuration. The review feedback highlights several improvement opportunities: making the geographic classification logic in process.py more robust, casting aggregated count statistics to integers (Int64) to prevent float representation, correcting a fallback test data copy that mixes state and county resolutions, ensuring output verification checks for non-empty files, and replacing the file command in download.sh with a more portable unzip -t check.

Comment thread scripts/us_cdc/natality/process.py Outdated
Comment on lines +105 to +111
sample_geo = (sample_df['Geo'].dropna().iloc[0] if
not sample_df['Geo'].dropna().empty else '')
# State DCIDs are geoId/XX (length 9), County DCIDs are geoId/XXXXX (length 12)
if len(sample_geo) <= 9:
state_csvs.append(f)
else:
county_csvs.append(f)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current classification logic relies on len(sample_geo) <= 9 to distinguish between state and county DCIDs. However, this is fragile: an empty sample_geo (length 0) or a country DCID like country/USA (length 11) could be misclassified. Since US state DCIDs are always geoId/XX (length 8) and county DCIDs are geoId/XXXXX (length 11), we can make this check much more robust by explicitly matching these patterns.

Suggested change
sample_geo = (sample_df['Geo'].dropna().iloc[0] if
not sample_df['Geo'].dropna().empty else '')
# State DCIDs are geoId/XX (length 9), County DCIDs are geoId/XXXXX (length 12)
if len(sample_geo) <= 9:
state_csvs.append(f)
else:
county_csvs.append(f)
sample_geo = (sample_df['Geo'].dropna().iloc[0] if
not sample_df['Geo'].dropna().empty else '')
# State DCIDs are geoId/XX (length 8), County DCIDs are geoId/XXXXX (length 11)
if sample_geo.startswith('country/'):
country_csvs.append(f)
elif sample_geo.startswith('geoId/') and len(sample_geo) == 11:
county_csvs.append(f)
elif sample_geo.startswith('geoId/') and len(sample_geo) == 8:
state_csvs.append(f)
else:
logging.warning(f'Could not determine geo level for {f} with geo {sample_geo}')
References
  1. A dcid is a unique identifier that maps to a single, consistent set of properties. Ensuring correct identification of DCIDs (like state vs county) prevents mapping issues.

Comment on lines +141 to +146
count_df['Quantity'] = pd.to_numeric(count_df['Quantity'],
errors='coerce')
country_df = count_df.groupby(['Year', 'StatVar'],
as_index=False)['Quantity'].sum()
country_df.sort_values(by=['Year', 'StatVar'], inplace=True)
country_df.to_csv(country_out, index=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When converting Quantity to numeric and performing a .sum(), pandas will represent the resulting values as floats (e.g., 3613647.0). For count statistics, we should ensure they are formatted as integers in the output CSV to prevent schema/type mismatch issues during ingestion. Casting the column to the nullable integer type Int64 resolves this.

Suggested change
count_df['Quantity'] = pd.to_numeric(count_df['Quantity'],
errors='coerce')
country_df = count_df.groupby(['Year', 'StatVar'],
as_index=False)['Quantity'].sum()
country_df.sort_values(by=['Year', 'StatVar'], inplace=True)
country_df.to_csv(country_out, index=False)
count_df['Quantity'] = pd.to_numeric(count_df['Quantity'],
errors='coerce')
country_df = count_df.groupby(['Year', 'StatVar'],
as_index=False)['Quantity'].sum()
country_df['Quantity'] = country_df['Quantity'].astype('Int64')
country_df.sort_values(by=['Year', 'StatVar'], inplace=True)
country_df.to_csv(country_out, index=False)

Comment thread scripts/us_cdc/natality/process.py Outdated
Comment on lines +215 to +217
county_csv = os.path.join(output_path, 'county.csv')
if not os.path.exists(county_csv) and os.path.exists(tmp_state_csv):
shutil.copyfile(tmp_state_csv, county_csv)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Copying state-level data (tmp_state_csv) directly to county_csv is incorrect because they represent different geographic resolutions (state DCIDs like geoId/01 vs county DCIDs like geoId/01003). This will result in invalid county-level data if the pipeline is run with --use_test_data. Consider running the preprocessing script with the county config if county test data is available, or at least log a warning/error instead of silently copying mismatched geographic data.

Comment on lines +228 to +231
f for f in required_outputs
if not os.path.exists(os.path.join(output_path, f))
]
if missing:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment above states that the pipeline verifies that all required output files exist and are non-empty. However, the code only checks os.path.exists. To align with the comment and prevent downstream ingestion of empty files, we should also verify that the file size is greater than 0.

    missing = [
        f for f in required_outputs
        if not os.path.exists(os.path.join(output_path, f)) or os.path.getsize(os.path.join(output_path, f)) == 0
    ]

Comment thread scripts/us_cdc/natality/download.sh Outdated
Comment on lines +49 to +54
if [ -f "$SCRIPT_DIR/source_data/county.zip" ]; then
if file "$SCRIPT_DIR/source_data/county.zip" | grep -q "Zip archive"; then
echo "Extracting raw county source data..."
unzip -q -o "$SCRIPT_DIR/source_data/county.zip" -d "$INPUT_DIR/" || true
fi
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file command is often not installed in minimal execution environments (such as standard Docker containers used in CI/CD or Cloud Batch). Using unzip -t is a much more portable and standard way to verify if a file is a valid zip archive.

Suggested change
if [ -f "$SCRIPT_DIR/source_data/county.zip" ]; then
if file "$SCRIPT_DIR/source_data/county.zip" | grep -q "Zip archive"; then
echo "Extracting raw county source data..."
unzip -q -o "$SCRIPT_DIR/source_data/county.zip" -d "$INPUT_DIR/" || true
fi
fi
if [ -f "$SCRIPT_DIR/source_data/county.zip" ]; then
if unzip -t "$SCRIPT_DIR/source_data/county.zip" &>/dev/null; then
echo "Extracting raw county source data..."
unzip -q -o "$SCRIPT_DIR/source_data/county.zip" -d "$INPUT_DIR/" || true
fi
fi

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces automation scripts, configurations, and tests for the CDC Wonder Natality import, including an end-to-end processing pipeline (process.py) and a download script (download.sh). Feedback on the new pipeline highlights critical issues in process.py: the geographic classification logic for sample_geo contains flaws regarding DCID lengths and empty values; converting the Quantity column to numeric can result in float representations instead of integers; and copying state-level data to county-level output as a fallback introduces mismatched geographic associations.

Comment thread scripts/us_cdc/natality/process.py Outdated
Comment on lines +105 to +111
sample_geo = (sample_df['Geo'].dropna().iloc[0] if
not sample_df['Geo'].dropna().empty else '')
# State DCIDs are geoId/XX (length 9), County DCIDs are geoId/XXXXX (length 12)
if len(sample_geo) <= 9:
state_csvs.append(f)
else:
county_csvs.append(f)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The classification logic based on the length of sample_geo has a few issues:

  1. Incorrect Lengths in Comment: State DCIDs like geoId/01 have a length of 8 (not 9), and County DCIDs like geoId/01001 have a length of 11 (not 12).
  2. Empty Geo Handling: If sample_df['Geo'] is entirely empty/NaN, sample_geo becomes '' (length 0), which is <= 9 and will be incorrectly classified as a state CSV.
  3. Country DCID Misclassification: If sample_geo is country/USA (length 11), it is > 9 and will be incorrectly classified as a county CSV.

Consider a more robust classification approach, such as checking if the DCID starts with geoId/ and then checking the length of the numeric part, or checking for country explicitly.

Suggested change
sample_geo = (sample_df['Geo'].dropna().iloc[0] if
not sample_df['Geo'].dropna().empty else '')
# State DCIDs are geoId/XX (length 9), County DCIDs are geoId/XXXXX (length 12)
if len(sample_geo) <= 9:
state_csvs.append(f)
else:
county_csvs.append(f)
sample_geo = (sample_df['Geo'].dropna().iloc[0] if
not sample_df['Geo'].dropna().empty else '')
if not sample_geo:
logging.warning(f'Could not classify {f}: Geo column is empty')
elif 'country' in sample_geo or 'USA' in sample_geo:
country_csvs.append(f)
elif sample_geo.startswith('geoId/'):
parts = sample_geo.split('/')
if len(parts) > 1 and len(parts[1]) == 2:
state_csvs.append(f)
else:
county_csvs.append(f)
else:
logging.warning(f'Could not classify {f} with Geo: {sample_geo}')

Comment on lines +141 to +142
count_df['Quantity'] = pd.to_numeric(count_df['Quantity'],
errors='coerce')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Converting Quantity to numeric without specifying an integer type can result in float representation (e.g., 3613647.0) in the generated country.csv if there are any missing values or due to pandas default upcasting. Since these are birth counts, they should be represented as integers.

Using pandas' nullable integer type Int64 (capital 'I') will preserve integer formatting while safely handling any potential NaN values.

Suggested change
count_df['Quantity'] = pd.to_numeric(count_df['Quantity'],
errors='coerce')
count_df['Quantity'] = pd.to_numeric(count_df['Quantity'],
errors='coerce').astype('Int64')

Comment thread scripts/us_cdc/natality/process.py Outdated
Comment on lines +215 to +217
county_csv = os.path.join(output_path, 'county.csv')
if not os.path.exists(county_csv) and os.path.exists(tmp_state_csv):
shutil.copyfile(tmp_state_csv, county_csv)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Copying state.csv to county.csv as a fallback is conceptually incorrect because state-level data contains state DCIDs (e.g., geoId/01), whereas county-level data is expected to contain county DCIDs (e.g., geoId/01003). Processing state-level data with the county TMCF mapping (county/output.tmcf) will result in invalid or mismatched geographic associations in Data Commons.

If county-level test data is not available or cannot be generated, it is better to skip generating county.csv in the fallback path or raise a clear warning/error, rather than copying mismatched geographic data.

- Use distinct output TMCF paths (country.tmcf, state.tmcf, county.tmcf) in manifest.json to prevent Cloud Batch GCS basename collision.
- Remove dangling validation_config_file from manifest.json and README.md.
- Ensure chronological sorting and filename-based routing in process.py.
- Use regex Geo validation (geoId/XX vs geoId/XXXXX) and cast aggregated quantities to Int64.
- Tighten error handling in download.sh: require gcloud, remove silent failure suppression, and verify all geographic resolution levels.
- Track Buganizer issue b/555054352.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant