Skip to main content

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:

  1. Frequency-based tag creation: Dynamically detect reading frequency and create appropriate tags
  2. LPS demand tags: Create parallel liters-per-second tags for hydraulic modeling

Prerequisites

1. Environment Setup

  • Ensure Python 3.8+ is installed
  • Update requirements.txt with 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_SERIAL
    • READING_YEAR
    • READING_PERIOD
    • CONSUMPTION

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.py

  • Import 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_SERIAL and READING_YEAR
    • Count unique READING_PERIOD per group
    • Classify frequency using FREQUENCY_MAP
    • Return frequency analysis dataframe
  • 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}_consumption
      • TagType: {frequency}_consumption
      • Unit: cubic feet
      • DeviceId: from devices_df
    • Return list of tag dictionaries
  • Implement bulk_create_device_tags(cursor, device_tags)

    • Use executemany() or execute_values() for performance
    • Handle ON CONFLICT if tags already exist
    • Log number of tags created
    • Return created tag IDs

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)

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

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:

    1. Parse command-line arguments (--csv, --dry-run, --db-config)
    2. Setup logging
    3. Load CSV data
    4. Analyze frequencies
    5. Identify device tag needs
    6. (If not dry-run) Create DeviceTags
    7. (If not dry-run) Create DeviceTagDatum records
    8. Generate summary report
    9. 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_SECONDS dict
    • 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_lps
        • TagType: {frequency}_demand_lps
        • Unit: liters/second
        • DeviceId: same as consumption tag
    • Return combined list (2x tags)

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
    • 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.py with 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.py on 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.csv
  • Document 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

PhaseDurationDependencies
1. Analysis Script1-2 daysNone
2. Core Implementation3-5 daysPhase 1 complete
3. LPS Integration2-3 daysPhase 2 complete
4. Testing2-3 daysPhase 3 complete
5. Production Deployment1-2 daysPhase 4 complete, approvals
6. Documentation & Handoff1 dayPhase 5 complete
Total10-16 days~2-3 weeks

Risk Mitigation

Technical Risks

RiskImpactLikelihoodMitigation
Large dataset causes memory issuesHighMediumUse pandas chunking, process in batches
Conversion calculations are incorrectHighLowExtensive testing, manual validation
Database locks during bulk insertsMediumMediumUse smaller batch sizes, run during off-hours
Existing data conflictsMediumMediumImplement robust ON CONFLICT handling
Unexpected frequency patterns in dataMediumMediumAnalysis phase identifies issues early

Process Risks

RiskImpactLikelihoodMitigation
Timeline slipsMediumMediumBuilt-in buffer time, prioritize core features
Requirements change mid-developmentMediumLowModular design allows easy adjustments
Data quality issues discovered lateHighLowPhase 1 analysis catches issues early
Production deployment failsHighLowDry-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:

  1. Identify the issue

    • Review logs
    • Check validation queries
    • Identify affected records
  2. Assess impact

    • How many devices affected?
    • Is data corrupted or just incomplete?
    • Can we fix in-place or need to rollback?
  3. Rollback procedure (if needed)

    • Restore database from backup
    • Re-run with corrected script
    • Validate results
  4. 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 script
  • v2/parse_timeseries_csv_v2.py - Main processing script
  • IMPLEMENTATION_PLAN.md - This document

Modified Files

  • DEVICE_TIMESERIES_UPGRADE_OCTOBER_2025.md - Updated with LPS specification
  • README.md - Updated with new functionality
  • requirements.txt - Updated with pandas/numpy dependencies

Database Tables

  • Devices - Read for SerialNumber to Id mapping
  • DeviceTags - Created/updated by script
  • DeviceTagData - 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