Skip to content

fix wrong timezone assumptions and adapt tests - #90

Merged
maxnutz merged 5 commits into
mainfrom
89-timedelta-in-evaluation-of-timesteps-leads-to-wrong-timesteps-on-scenario-explorer
Jul 9, 2026
Merged

fix wrong timezone assumptions and adapt tests#90
maxnutz merged 5 commits into
mainfrom
89-timedelta-in-evaluation-of-timesteps-leads-to-wrong-timesteps-on-scenario-explorer

Conversation

@maxnutz

@maxnutz maxnutz commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Short description of this pull request

changes the timezone assumptions from hours+1 to hours+0, as scenario explorer itself creates respective timezone data leading to "double counting".


Checklist

Before asking for review, please make shure, the following steps are completed (whenever possible):

  • Changes are tested locally and behave as expected.
  • Code is documented using numpy-styled function docstrings
  • All tests succeed
  • All Sourcery-bot review suggestions have been implemented or rejected with an explanation.

Sourcery-Bot starts to review your pull request, whenever it is created. This may take some time. After having finished these steps, please request for review in the Pull Request.

Summary by Sourcery

Adjust timestamp handling to avoid incorrect timezone offsets and improve support for yearly aggregated data.

Bug Fixes:

  • Change timestamp normalization from UTC+01:00 to UTC+00:00 to prevent double-counting due to conflicting timezone assumptions.

Enhancements:

  • Handle yearly aggregated data by using integer year column labels instead of localized timestamps.
  • Update default configuration to reference the current PyPSA-AT model version.

Tests:

  • Update timestamp-related tests to reflect the new UTC+00:00 behavior and support integer year column labels.

@maxnutz maxnutz linked an issue Jul 8, 2026 that may be closed by this pull request
2 tasks
@sourcery-ai

sourcery-ai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR fixes incorrect timezone handling by standardizing timestamp localization to UTC+00:00, refines how timestamp-like columns are formatted (including a special path for yearly-aggregated data), updates corresponding tests to expect the new behavior, and bumps the default config’s model name.

Flow diagram for updated format_timestamps behavior

flowchart TD
    A[Input df with columns] --> B[Create fixed_tz = UTC+00:00]
    B --> C[Get cols and idx_name]
    C --> D[Parse column labels with pd.to_datetime]

    D --> E{All column labels are 4-digit year strings?}

    E -->|Yes| F[Convert labels to int years]
    F --> G[Set df.columns to integer Index]
    G --> H[Return df]

    E -->|No| I[Iterate over columns]
    I --> J[For each col: check if year-only string]
    J --> K[Determine ts from parsed value or year start]
    K --> L{ts.tz is not None?}

    L -->|Yes| M[Use ts directly as column label]
    M --> O[Collect converted labels]

    L -->|No| N[Localize ts to fixed_tz]
    N --> P{Localization failed?}
    P -->|Yes| Q[Set label to pd.NaT and record warning]
    P -->|No| R[Use ts_tz as column label]
    Q --> O
    R --> O

    O --> S[Convert cols Index to Python datetime objects]
    S --> T[Set df.columns to object Index]
    T --> U[Print NaT warnings if any]
    U --> V[Return df]
Loading

File-Level Changes

Change Details Files
Standardize timestamp localization logic to use UTC+00:00 and add a branch for yearly-aggregated data labels.
  • Change the fixed_tz used for localization from UTC+01:00 to UTC+00:00.
  • Introduce a fast-path that detects columns representing only years and converts them to integer labels instead of timestamps.
  • Restrict the timestamp-conversion loop to non-yearly data and keep tracking of converted and NaT columns, preserving warning behavior.
  • Continue to convert non-yearly column labels to tz-aware Python datetime objects and assign them back to the DataFrame columns.
pypsa_validation_processing/class_definitions.py
Align tests with the new timezone and column-label semantics.
  • Update tz-aware timestamp in tests to use UTC+00:00 instead of UTC+01:00.
  • Extend the type check for time columns in the pyam integration test to allow integer labels for yearly-aggregated columns.
  • Remove the previous assertion that expected a +1 hour utcoffset on time columns.
tests/test_format_timestamps.py
Update default configuration metadata for the PyPSA model.
  • Change the default model_name to the new Pypsa-AT v1.0 identifier in the config file.
pypsa_validation_processing/configs/config.default.yaml

Assessment against linked issues

Issue Objective Addressed Explanation
#89 Adjust timestamp processing to use a 0-hour UTC offset instead of +1 hour to avoid Scenario Explorer subtracting an extra hour.
#89 For yearly aggregated data, convert timestamp-like column labels to simple year values (e.g., 2020, 2030) instead of full timestamps.

Possibly linked issues

  • #General processing adaptations for Scenario Explorer: PR switches timedelta to UTC+0 and converts yearly aggregated columns to integers, matching Scenario Explorer processing needs.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@maxnutz
maxnutz marked this pull request as ready for review July 9, 2026 12:02

@sourcery-ai sourcery-ai 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.

Hey - I've found 4 issues, and left some high level feedback:

  • The new special-case for yearly aggregated data duplicates the year-only detection logic; consider extracting a shared helper or reusing the regex result to avoid running similar checks twice and keep the flow easier to follow.
  • Using print for warnings in format_timestamps makes it hard to control output in larger workflows; switching to the standard logging framework would allow callers to configure log levels and handlers more flexibly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new special-case for yearly aggregated data duplicates the year-only detection logic; consider extracting a shared helper or reusing the regex result to avoid running similar checks twice and keep the flow easier to follow.
- Using `print` for warnings in `format_timestamps` makes it hard to control output in larger workflows; switching to the standard logging framework would allow callers to configure log levels and handlers more flexibly.

## Individual Comments

### Comment 1
<location path="pypsa_validation_processing/class_definitions.py" line_range="55-58" />
<code_context>
-    for i, col in enumerate(cols):
-        is_year_only = isinstance(col, str) and re.match(r"^\d{4}$", col) is not None
-        parsed_value = parsed[i]
+    # for yearly aggregated data, use integers as column labels.
+    if all([(isinstance(elem, str) and re.match(r"^\d{4}$", elem)) for elem in cols]):
+        converted_list = [int(col) for col in cols]
+        df.columns = pd.Index(converted_list, name=idx_name)
+    # for non-aggregated data, use timestamps as column labels.
+    else:
</code_context>
<issue_to_address>
**issue (bug_risk):** Behavior for yearly columns now contradicts the function’s timestamp-focused contract.

The yearly-aggregation branch now returns integer column labels instead of tz-aware timestamps, while the function’s name and docstring still promise normalization to UTC+00:00 datetimes. This mismatch can cause subtle bugs for callers that depend on tz-aware columns. Please either keep yearly labels as localized timestamps, or explicitly update the function contract and docstring to document the integer-label behavior so consumers can handle both cases correctly.
</issue_to_address>

### Comment 2
<location path="tests/test_format_timestamps.py" line_range="73-75" />
<code_context>
 def test_format_timestamps_preserves_tz_aware_columns():
     aware_label = pd.Timestamp(
         "2050-01-01 00:00:00",
-        tz=datetime.timezone(datetime.timedelta(hours=1)),
+        tz=datetime.timezone(datetime.timedelta(hours=0)),
     )
     df = pd.DataFrame([[1.0]], columns=[aware_label])
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case that verifies tz-aware columns with a non-UTC offset are preserved unchanged.

The existing test now only covers a tz-aware timestamp with the same UTC+00:00 offset that `format_timestamps` applies. Please add another test using a different timezone (e.g. UTC+02 or `Europe/Berlin`) and assert that `format_timestamps` leaves that timezone intact. This will help catch regressions where tz-aware inputs are incorrectly normalized to UTC.
</issue_to_address>

### Comment 3
<location path="tests/test_format_timestamps.py" line_range="130-133" />
<code_context>
     time_columns = [
         c
         for c in passed_data.columns
-        if isinstance(c, (pd.Timestamp, datetime.datetime))
+        if isinstance(c, (pd.Timestamp, datetime.datetime, int))
     ]
     assert len(time_columns) == 1
</code_context>
<issue_to_address>
**suggestion (testing):** Extend this test to assert the expected type and timezone/offset of the time column, not just its count.

Including `int` in `time_columns` means this test only verifies that there is one time-like column, not that it has the correct representation. To ensure the timezone fix is covered, the test should assert that for non-aggregated data the time column is a tz-aware UTC timestamp, and for yearly aggregated data it is an `int` year (not a timestamp). Splitting into separate tests for hourly/non-aggregated and yearly/aggregated input and asserting both the type and, where applicable, `utcoffset()`, would make this behavior explicit.

Suggested implementation:

```python
def test_format_timestamps_keeps_unparsable_columns_hourly():
    # Non-aggregated/hourly data: time column should be tz-aware UTC timestamp
    time_columns = [
        c
        for c in passed_data.columns
        if isinstance(c, (pd.Timestamp, datetime.datetime, int))
    ]
    # Ensure the input has exactly one time-like column
    assert len(time_columns) == 1

    out = format_timestamps(passed_data)

    # After formatting, there should still be exactly one time-like column
    time_columns_out = [
        c
        for c in out.columns
        if isinstance(c, (pd.Timestamp, datetime.datetime, int))
    ]
    assert len(time_columns_out) == 1

    time_col = time_columns_out[0]
    # For non-aggregated data, the time column must be a tz-aware UTC timestamp
    assert isinstance(time_col, (pd.Timestamp, datetime.datetime))
    assert time_col.tzinfo is not None
    assert time_col.utcoffset() == datetime.timedelta(hours=0)


def test_format_timestamps_keeps_unparsable_columns_yearly():
    # Yearly/aggregated data: time column should be an integer year
    time_columns = [
        c
        for c in yearly_passed_data.columns
        if isinstance(c, (pd.Timestamp, datetime.datetime, int))
    ]
    # Ensure the input has exactly one time-like column
    assert len(time_columns) == 1

    out = format_timestamps(yearly_passed_data)

    # After formatting, there should still be exactly one time-like column
    time_columns_out = [
        c
        for c in out.columns
        if isinstance(c, (pd.Timestamp, datetime.datetime, int))
    ]
    assert len(time_columns_out) == 1

    time_col = time_columns_out[0]
    # For yearly aggregated data, the time column must be an integer year
    assert isinstance(time_col, int)

```

To fully implement the suggestion, you will likely need to:
1. Ensure `yearly_passed_data` is defined as a fixture or test data in this module (or imported) representing yearly/aggregated input, analogous to `passed_data` but with a yearly time column.
2. Confirm that `format_timestamps` actually produces an integer year for yearly/aggregated input; if it currently returns timestamps, update the implementation accordingly or adjust the fixture so it exercises the intended code path.
3. If there are multiple types of aggregated inputs (e.g. monthly, daily), consider adding similar explicit tests for those to keep the behavior clear and covered.
</issue_to_address>

### Comment 4
<location path="tests/test_format_timestamps.py" line_range="136" />
<code_context>
+        if isinstance(c, (pd.Timestamp, datetime.datetime, int))
     ]
     assert len(time_columns) == 1
-    assert time_columns[0].utcoffset() == datetime.timedelta(hours=1)
</code_context>
<issue_to_address>
**issue (testing):** Replace the removed utcoffset assertion with one that validates the new UTC+00:00 behavior where applicable.

The original test ensured tz-aware timestamps with a +1 hour offset; with that removed, we should still assert the new UTC+00:00 behavior for non-aggregated timestamps. If `structure_pyam_from_pandas` now produces UTC tz-aware hourly data, please add an assertion on `time_columns[0].utcoffset()` for that case. If aggregated data intentionally uses naïve integer years instead, add a separate test for that path so both behaviors and the timezone normalization fix are clearly covered.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread pypsa_validation_processing/class_definitions.py
Comment thread tests/test_format_timestamps.py
Comment thread tests/test_format_timestamps.py
Comment thread tests/test_format_timestamps.py
@maxnutz
maxnutz merged commit 7ad066f into main Jul 9, 2026
2 checks passed
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.

General processing adaptations for Scenario Explorer

1 participant