Implementation Plan: Timeseries Processing Upgrade with LPS Tags
Overview
This implementation plan covers the development of parse_timeseries_csv_v2.py with two major upgrades:
- Frequency-based tag creation: Dynamically detect reading frequency and create appropriate tags
- LPS demand tags: Create parallel liters-per-second tags for hydraulic modeling
Prerequisites
1. Environment Setup
- Ensure Python 3.8+ is installed
- Update
requirements.txtwith new dependencies:pandas>=2.0.0
numpy>=1.24.0
psycopg2-binary>=2.9.0 - Run
pip install -r requirements.txt
2. Database Access
- Verify PostgreSQL connection details
- Ensure write access to DeviceTags and DeviceTagData tables
- Consider using test database for initial runs
3. Input Data
- Identify timeseries CSV file location
- Verify CSV encoding (cp1252)
- Confirm required columns exist:
REG_SERIALREADING_YEARREADING_PERIODCONSUMPTION
Phase 1: Analysis Script (1-2 days)
Objective
Create a read-only analysis script to profile existing data and validate design assumptions.
Tasks
1.1 Create v2/analyze_frequency_patterns.py
"""
Analysis script to profile timeseries data frequency patterns.
NO database writes - read-only analysis.
"""
Note: All new scripts will be created in the v2/ subdirectory. Legacy scripts remain in v1/ for reference.
Functions to implement:
-
load_csv(): Load CSV with proper encoding -
analyze_frequencies(): Count periods per device per year -
classify_frequency(): Map period counts to frequency types -
generate_summary_report(): Create human-readable report -
identify_anomalies(): Flag unusual period counts (3, 5-11, >12) -
find_mixed_frequency_devices(): List devices with multiple frequencies
1.2 Generate Analysis Report
- Run analysis on full dataset:
python v2/analyze_frequency_patterns.py path/to/timeseries.csv - Review output report covering:
- Total devices analyzed
- Frequency distribution (annual/semiannual/quarterly/monthly counts)
- Devices with mixed frequencies (% and count)
- Anomalies (unexpected period counts)
- Sample device timelines (show frequency transitions)
1.3 Validate Assumptions
- Confirm frequency classification logic matches real data
- Verify timestamp conversion will work for all frequencies
- Identify any data quality issues requiring special handling
Deliverables:
v2/analyze_frequency_patterns.py- Analysis report (text or CSV)
- Decision on handling anomalies
Phase 2: Core Implementation (3-5 days)
Objective
Develop v2/parse_timeseries_csv_v2.py with frequency detection and consumption tag creation.
Architecture
v2/parse_timeseries_csv_v2.py
├── Configuration
│ ├── Database connection
│ ├── Table names
│ └── Constants (conversion factors)
├── Data Loading
│ └── load_csv_to_dataframe()
├── Frequency Analysis
│ ├── analyze_frequency_patterns()
│ ├── classify_frequency()
│ └── validate_frequency_data()
├── DeviceTag Creation
│ ├── identify_device_tag_needs()
│ ├── generate_device_tags()
│ └── bulk_create_device_tags()
├── DeviceTagDatum Creation
│ ├── convert_reading_to_unix_time()
│ ├── prepare_datum_records()
│ └── bulk_create_device_tag_data()
├── Logging & Reporting
│ ├── setup_logging()
│ ├── log_progress()
│ └── generate_summary_report()
└── Main Execution
├── Dry-run mode (--dry-run)
└── Production mode
Tasks
2.1 Setup & Configuration
Create
v2/parse_timeseries_csv_v2.pyImport dependencies (pandas, psycopg2, datetime, logging)
Define configuration constants:
# Database tables
DEVICES_TABLE = "Devices"
DEVICE_TAGS_TABLE = "DeviceTags"
DEVICE_TAG_DATA_TABLE = "DeviceTagData"
# Frequency classification
FREQUENCY_MAP = {
1: 'annual',
2: 'semiannual',
4: 'quarterly',
12: 'monthly'
}Setup logging with appropriate verbosity levels
2.2 Data Loading Module
- Implement
load_csv_to_dataframe(csv_path)- Handle cp1252 encoding
- Validate required columns exist
- Handle missing/null values
- Log data summary (rows, devices, year range)
2.3 Frequency Analysis Module
Implement
analyze_frequency_patterns(df)- Group by
REG_SERIALandREADING_YEAR - Count unique
READING_PERIODper group - Classify frequency using
FREQUENCY_MAP - Return frequency analysis dataframe
- Group by
Implement
classify_frequency(period_count)- Map period count to frequency type
- Log warnings for unexpected counts
- Return frequency string or 'unknown_X'
Implement
validate_frequency_data(df)- Check for duplicate (device, year, period) combinations
- Identify gaps in period sequences
- Log data quality issues
2.4 DeviceTag Creation Module
Implement
identify_device_tag_needs(df, frequency_df)- Merge frequency analysis with main dataframe
- Get unique (device, frequency) combinations
- Return dataframe of needed tags
Implement
fetch_device_mappings(cursor)- Query Devices table for Id, SerialNumber, Name
- Return as pandas DataFrame
- Log devices not found in database
Implement
generate_device_tags(unique_combinations, devices_df)- Create consumption tag records:
Name:{DeviceName}_{frequency}_consumptionTagType:{frequency}_consumptionUnit:cubic feetDeviceId: from devices_df
- Return list of tag dictionaries
- Create consumption tag records:
Implement
bulk_create_device_tags(cursor, device_tags)- Use
executemany()orexecute_values()for performance - Handle ON CONFLICT if tags already exist
- Log number of tags created
- Return created tag IDs
- Use
2.5 Timestamp Conversion Module
- Implement
convert_reading_to_unix_time(year, period)- Map period to month (quarterly mapping as default):
- Period 1 → January 15
- Period 2 → April 15
- Period 3 → July 15
- Period 4 → October 15
- For monthly data: period = month
- Use UTC timezone
- Return Unix timestamp (integer)
- Map period to month (quarterly mapping as default):
2.6 DeviceTagDatum Creation Module
Implement
prepare_datum_records(df, frequency_df, device_tags_db)- Merge frequency type back to main dataframe
- Merge device mappings
- Merge DeviceTag IDs
- Convert to Unix timestamps
- Return list of (DeviceTagId, UnixTime, Value) tuples
Implement
bulk_create_device_tag_data(cursor, datum_records)- Use
executemany()with ON CONFLICT - Batch inserts if dataset is large (chunks of 10k)
- Log progress periodically
- Return number of records created/updated
- Use
2.7 Reporting Module
- Implement
generate_summary_report(stats)- Total devices processed
- Total DeviceTags created
- Frequency distribution
- Total DeviceTagDatum records created
- Processing time
- Any errors or warnings
- Save to file and log to console
2.8 Main Execution Flow
Implement
main()function:- Parse command-line arguments (--csv, --dry-run, --db-config)
- Setup logging
- Load CSV data
- Analyze frequencies
- Identify device tag needs
- (If not dry-run) Create DeviceTags
- (If not dry-run) Create DeviceTagDatum records
- Generate summary report
- Close database connection
Add dry-run mode:
- All analysis runs normally
- No database writes
- Print what would be created
- Show sample records
Deliverables:
v2/parse_timeseries_csv_v2.py(functional, with consumption tags only)- Unit tests for key functions
- Documentation strings for all functions
Phase 3: LPS Tag Integration (2-3 days)
Objective
Add parallel LPS (liters per second) tag creation to the existing implementation.
Tasks
3.1 Add Conversion Constants
Add to configuration section:
# Unit conversion constants
CUBIC_FEET_TO_LITERS = 28.3168
# Time period constants (seconds)
SECONDS_PER_MONTH = 30.44 * 24 * 3600 # 2,629,440
SECONDS_PER_QUARTER = 3 * SECONDS_PER_MONTH # 7,888,320
SECONDS_PER_SEMIANNUAL = 6 * SECONDS_PER_MONTH # 15,776,640
SECONDS_PER_ANNUAL = 365.25 * 24 * 3600 # 31,557,600
PERIOD_SECONDS = {
'monthly': SECONDS_PER_MONTH,
'quarterly': SECONDS_PER_QUARTER,
'semiannual': SECONDS_PER_SEMIANNUAL,
'annual': SECONDS_PER_ANNUAL
}
3.2 Implement Conversion Function
- Create
convert_cuft_to_lps(consumption_cuft, frequency_type)- Convert cubic feet to liters
- Get period seconds from
PERIOD_SECONDSdict - Divide liters by seconds
- Return lps value (float)
- Handle unknown frequency types gracefully
3.3 Modify DeviceTag Generation
- Update
generate_device_tags()to create both tag types:- For each (device, frequency) combination:
- Create consumption tag (existing logic)
- Create lps tag:
Name:{DeviceName}_{frequency}_demand_lpsTagType:{frequency}_demand_lpsUnit:liters/secondDeviceId: same as consumption tag
- Return combined list (2x tags)
- For each (device, frequency) combination:
3.4 Modify Datum Record Preparation
- Update
prepare_datum_records()to handle both tag types:- Fetch all DeviceTags (consumption + lps) from database
- Separate into two dataframes by TagType suffix
- Prepare consumption records (existing logic)
- Prepare lps records:
- Apply
convert_cuft_to_lps()to each record - Use same UnixTime as consumption records
- Store converted value
- Apply
- Return combined list of records
3.5 Update Reporting
- Modify
generate_summary_report()to show:- Total consumption tags created
- Total lps tags created
- Ratio should be 1:1
- Sample conversion calculations for validation
3.6 Add Validation Tests
- Create
validate_lps_conversions()function:- Spot-check sample conversions
- Compare against manual calculations
- Verify lps values are reasonable (not negative, not absurdly large)
- Log any suspicious values
Deliverables:
- Updated
v2/parse_timeseries_csv_v2.pywith LPS support - Validation tests for conversion accuracy
- Updated documentation
Phase 4: Testing (2-3 days)
Objective
Thoroughly test the implementation on progressively larger datasets.
Test Strategy
4.1 Unit Testing
Test frequency classification with edge cases:
- Single period per year → annual
- Two periods → semiannual
- Four periods → quarterly
- Twelve periods → monthly
- Unexpected counts (3, 5-11, >12)
Test timestamp conversion:
- Verify January 15th for period 1
- Verify quarterly mappings
- Test year boundaries (2000, 2023, etc.)
Test LPS conversion:
- Annual: 1000 cu ft over 1 year → expected lps
- Quarterly: 250 cu ft over 3 months → expected lps
- Monthly: 83.33 cu ft over 1 month → expected lps
- Verify conversion factor is correct
4.2 Integration Testing with Subset
Create test CSV with known data:
- Device A: Annual readings (5 years)
- Device B: Quarterly readings (2 years)
- Device C: Mixed (annual → quarterly transition)
- Device D: Monthly readings (1 year)
Run script on test data (dry-run mode)
- Verify correct frequency classification
- Verify tag names are correct
- Verify datum count matches expectations
Run script on test database
- Verify DeviceTags created correctly
- Verify DeviceTagDatum records created
- Query database to validate data
- Verify consumption and lps values are paired correctly
4.3 Validation Queries
Write SQL queries to validate results:
-- Check tag pairs (should be 1:1 ratio)
SELECT
COUNT(CASE WHEN "TagType" LIKE '%_consumption' THEN 1 END) as consumption_tags,
COUNT(CASE WHEN "TagType" LIKE '%_demand_lps' THEN 1 END) as lps_tags
FROM "DeviceTags";
-- Check datum count per tag type
SELECT
dt."TagType",
COUNT(*) as datum_count
FROM "DeviceTagData" dtd
JOIN "DeviceTags" dt ON dtd."DeviceTagId" = dt."Id"
GROUP BY dt."TagType"
ORDER BY dt."TagType";
-- Verify consumption and lps have same timestamps for a device
SELECT
d."Name",
dt_cons."Name" as consumption_tag,
dt_lps."Name" as lps_tag,
dtd_cons."UnixTime",
dtd_lps."UnixTime",
dtd_cons."Value" as cuft,
dtd_lps."Value" as lps
FROM "Devices" d
JOIN "DeviceTags" dt_cons ON d."Id" = dt_cons."DeviceId"
AND dt_cons."TagType" LIKE '%_consumption'
JOIN "DeviceTags" dt_lps ON d."Id" = dt_lps."DeviceId"
AND dt_lps."TagType" LIKE '%_demand_lps'
AND REPLACE(dt_cons."TagType", '_consumption', '') = REPLACE(dt_lps."TagType", '_demand_lps', '')
JOIN "DeviceTagData" dtd_cons ON dt_cons."Id" = dtd_cons."DeviceTagId"
JOIN "DeviceTagData" dtd_lps ON dt_lps."Id" = dtd_lps."DeviceTagId"
AND dtd_cons."UnixTime" = dtd_lps."UnixTime"
WHERE d."Name" = 'WTRMN00345'
ORDER BY dtd_cons."UnixTime";Manually verify sample calculations:
- Pick 5 random records
- Calculate expected lps by hand
- Compare with database values
- Tolerance: ±0.01% due to floating point
4.4 Performance Testing
Test on medium dataset (1000 devices, ~10k records)
- Measure processing time
- Monitor memory usage
- Target: < 1 minute
Test on large dataset (10k devices, ~100k records)
- Measure processing time
- Monitor memory usage
- Target: < 10 minutes
Identify bottlenecks if performance is poor:
- Database inserts (try batch sizing)
- DataFrame merges (optimize join keys)
- Conversion calculations (vectorize with numpy)
Deliverables:
- Test suite with passing tests
- Test report documenting results
- Performance benchmarks
Phase 5: Production Deployment (1-2 days)
Objective
Run the script on production data and verify results.
Pre-Deployment Checklist
- All tests passing
- Code reviewed
- Documentation complete
- Database backup created
- Rollback plan documented
Deployment Steps
5.1 Pre-Production Validation
Run analysis script on production CSV
- Compare results with Phase 1 analysis
- Verify no unexpected changes in data
Run main script in dry-run mode
- Review what will be created
- Verify tag counts match expectations
- Check for any errors or warnings
5.2 Production Run (Option A: Clean Database)
If starting fresh or in test environment:
- Clear existing DeviceTag and DeviceTagDatum records (if appropriate)
- Run
parse_timeseries_csv_v2.pyon full dataset - Monitor progress (watch logs)
- Wait for completion
- Review summary report
5.3 Production Run (Option B: Incremental Update)
If adding to existing data:
- Ensure ON CONFLICT handling is correct
- Run script with appropriate conflict resolution
- Monitor for conflicts/duplicates
- Review update counts
5.4 Post-Deployment Validation
Run validation SQL queries (from Phase 4.3)
Verify total counts:
- DeviceTags created = 2x unique (device, frequency) combinations
- DeviceTagDatum records = 2x (one for consumption, one for lps)
Spot-check devices:
- Select 10 random devices
- Verify they have correct tags
- Verify datum values are reasonable
- Verify consumption/lps pairs match
Check for anomalies:
- Devices with missing tags
- Orphaned datum records
- Unexpected tag types
5.5 Generate Final Report
- Create production run report including:
- Total devices processed
- Total tags created (by type)
- Total datum records created
- Processing time
- Data quality issues encountered
- Validation results
- Sample queries with results
Deliverables:
- Production-ready
v2/parse_timeseries_csv_v2.py - Production run report
- Updated documentation
Phase 6: Documentation & Handoff (1 day)
Objective
Document the new system for future maintenance and use.
Tasks
6.1 Update README.md
Add section on frequency-based processing
Document LPS tag functionality
Add usage examples:
# Analysis only
python v2/analyze_frequency_patterns.py timeseries.csv
# Dry run
python v2/parse_timeseries_csv_v2.py --csv timeseries.csv --dry-run
# Production run
python v2/parse_timeseries_csv_v2.py --csv timeseries.csvDocument command-line arguments
Add troubleshooting section
6.2 Code Documentation
- Ensure all functions have docstrings
- Add inline comments for complex logic
- Document conversion formulas with references
6.3 Create Operational Guide
- Document when to run the script (quarterly? annually?)
- Document monitoring procedures
- Document common issues and solutions
- Document validation procedures
6.4 Knowledge Transfer
- Walk through code with team
- Demonstrate dry-run and production modes
- Show how to read logs and reports
- Show validation queries
Deliverables:
- Updated README.md
- Operational guide document
- Code with complete documentation
- Knowledge transfer session
Timeline Summary
| Phase | Duration | Dependencies |
|---|---|---|
| 1. Analysis Script | 1-2 days | None |
| 2. Core Implementation | 3-5 days | Phase 1 complete |
| 3. LPS Integration | 2-3 days | Phase 2 complete |
| 4. Testing | 2-3 days | Phase 3 complete |
| 5. Production Deployment | 1-2 days | Phase 4 complete, approvals |
| 6. Documentation & Handoff | 1 day | Phase 5 complete |
| Total | 10-16 days | ~2-3 weeks |
Risk Mitigation
Technical Risks
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Large dataset causes memory issues | High | Medium | Use pandas chunking, process in batches |
| Conversion calculations are incorrect | High | Low | Extensive testing, manual validation |
| Database locks during bulk inserts | Medium | Medium | Use smaller batch sizes, run during off-hours |
| Existing data conflicts | Medium | Medium | Implement robust ON CONFLICT handling |
| Unexpected frequency patterns in data | Medium | Medium | Analysis phase identifies issues early |
Process Risks
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Timeline slips | Medium | Medium | Built-in buffer time, prioritize core features |
| Requirements change mid-development | Medium | Low | Modular design allows easy adjustments |
| Data quality issues discovered late | High | Low | Phase 1 analysis catches issues early |
| Production deployment fails | High | Low | Dry-run validation, backup/rollback plan |
Success Criteria
Functional Requirements
- Document updated with LPS specification
- Analysis script successfully profiles data
- Main script correctly classifies frequencies
- Consumption tags created with proper naming
- LPS tags created with proper naming
- Datum records created for both tag types
- Conversion calculations are accurate
- Script handles mixed-frequency devices
Performance Requirements
- Process 100k records in < 10 minutes
- Memory usage stays reasonable (< 2GB)
- Database inserts use efficient bulk operations
Quality Requirements
- All unit tests pass
- Integration tests pass on sample data
- Manual validation confirms accuracy
- No data loss or corruption
- Comprehensive logging for debugging
- Clear error messages
Documentation Requirements
- Code is well-documented
- README.md is updated
- Operational guide is complete
- Team is trained on new system
Rollback Plan
If issues are discovered after production deployment:
Identify the issue
- Review logs
- Check validation queries
- Identify affected records
Assess impact
- How many devices affected?
- Is data corrupted or just incomplete?
- Can we fix in-place or need to rollback?
Rollback procedure (if needed)
- Restore database from backup
- Re-run with corrected script
- Validate results
Fix-in-place procedure (if possible)
- Write corrective SQL script
- Test on subset
- Apply to production
- Validate results
Appendix A: Key Files
Directory Structure
devices/
├── v1/ # Legacy scripts (reference only)
│ ├── parse_timeseries_csv.py
│ ├── parse_timeseries_csv_bulk.py
│ └── [other legacy scripts]
├── v2/ # New implementation
│ ├── analyze_frequency_patterns.py
│ └── parse_timeseries_csv_v2.py
├── DEVICE_TIMESERIES_UPGRADE_OCTOBER_2025.md
├── IMPLEMENTATION_PLAN.md
└── README.md
New Files Created
v2/analyze_frequency_patterns.py- Analysis scriptv2/parse_timeseries_csv_v2.py- Main processing scriptIMPLEMENTATION_PLAN.md- This document
Modified Files
DEVICE_TIMESERIES_UPGRADE_OCTOBER_2025.md- Updated with LPS specificationREADME.md- Updated with new functionalityrequirements.txt- Updated with pandas/numpy dependencies
Database Tables
Devices- Read for SerialNumber to Id mappingDeviceTags- Created/updated by scriptDeviceTagData- Created/updated by script
Appendix B: Conversion Formula Reference
Cubic Feet to Liters
1 cubic foot = 28.3168 liters
Seconds per Period
Month (30.44 days avg): 2,629,440 seconds
Quarter (3 months): 7,888,320 seconds
Semi-annual (6 months): 15,776,640 seconds
Annual (365.25 days): 31,557,600 seconds
LPS Calculation Example
Given:
- Consumption: 1000 cubic feet
- Frequency: Quarterly
Step 1: Convert to liters
1000 cu ft × 28.3168 L/cu ft = 28,316.8 liters
Step 2: Divide by seconds in quarter
28,316.8 L ÷ 7,888,320 seconds = 0.003589 L/s
Result: 0.003589 lps (average demand for that quarter)
Appendix C: Example Validation Calculations
Device: WTRMN00345, Annual Reading
Input:
- Year: 2015
- Period: 1
- Consumption: 15,000 cubic feet
- Frequency: Annual
Expected Consumption Tag:
- DeviceTag:
WTRMN00345_annual_consumption - UnixTime: 1421280000 (2015-01-15 00:00:00 UTC)
- Value: 15000.0
- Unit: cubic feet
Expected LPS Tag:
- DeviceTag:
WTRMN00345_annual_demand_lps - UnixTime: 1421280000 (2015-01-15 00:00:00 UTC)
- Value: 15000 × 28.3168 ÷ 31,557,600 = 0.01346 L/s
- Unit: liters/second
Device: WTRMN00789, Quarterly Reading
Input:
- Year: 2023
- Period: 2 (Q2)
- Consumption: 3,500 cubic feet
- Frequency: Quarterly
Expected Consumption Tag:
- DeviceTag:
WTRMN00789_quarterly_consumption - UnixTime: 1681516800 (2023-04-15 00:00:00 UTC)
- Value: 3500.0
- Unit: cubic feet
Expected LPS Tag:
- DeviceTag:
WTRMN00789_quarterly_demand_lps - UnixTime: 1681516800 (2023-04-15 00:00:00 UTC)
- Value: 3500 × 28.3168 ÷ 7,888,320 = 0.01256 L/s
- Unit: liters/second
Questions & Decisions Log
Q1: Should we delete existing tags before creating new ones?
Decision: TBD based on environment (test vs production)
- Test environment: Yes, clean slate
- Production: Depends on existing data state
Q2: How do we handle devices not in the Devices table?
Decision: Log them and export to CSV for investigation. Don't create orphan tags.
Q3: What if period count doesn't match any expected frequency?
Decision: Create tag with type unknown_{count}_consumption and log warning. Manual review needed.
Q4: Should monthly data use period = month for timestamp?
Decision: Yes, for monthly data period directly maps to month (1=Jan, 2=Feb, etc.)
Q5: How precise should LPS values be stored?
Decision: Use float/double precision in database. Display with 5 decimal places in reports.
Q6: Should we create lps tags for unknown frequencies?
Decision: Yes, but log warning. Any frequency can be converted to lps with appropriate period seconds.
Contact & Support
For questions or issues with this implementation:
- Review documentation in
DEVICE_TIMESERIES_UPGRADE_OCTOBER_2025.md - Check logs in
v2/parse_timeseries_csv_v2.log - Review validation queries in this plan
- Compare with legacy implementation in
v1/directory
End of Implementation Plan