TimezoneTransformer
Converts the DataFrame index from UTC (what every source returns) to a local time zone, and moves the values of daily columns to local midnight.
Basic Usage
from epftoolbox2.data.transformers import TimezoneTransformer
transformer = TimezoneTransformer(target_tz="Europe/Warsaw")df = transformer.transform(df)As part of a DataPipeline, target_tz can be left unset: it then inherits DataPipeline.timezone, so the zone
is written once instead of in both places.
pipeline = DataPipeline(timezone="Europe/Warsaw").add_transformer(TimezoneTransformer())Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
target_tz | str | None | Target IANA time zone, e.g. "Europe/Warsaw". None inherits DataPipeline.timezone when part of a pipeline. Transforming without either raises |
daily_columns | list[str] | None | None | Column names holding one value per day, matched by exact name or a f"_{name}" suffix (so a prefixed neighbour’s column, e.g. fi_load_forecast_daily_min, is recognised too). None means ["load_forecast_daily_min", "load_forecast_daily_max"] (ENTSO-E week-ahead forecast). A name matching nothing in the frame is skipped |
daily_anchor | str | None | None | Time zone whose calendar date a daily value belongs to: a UTC offset ("UTC+02:00", "+01:00", "UTC") or an IANA name. None uses the resolved target_tz itself |
Daily columns
Each daily_columns entry matches a column of that exact name, or one ending in f"_{name}", so a prefixed
neighbour’s column (EntsoeSource(..., prefix="fi") gives fi_load_forecast_daily_min) is recognised under
the default list without adding every prefix by name.
ENTSO-E’s week-ahead load forecast (A31) has one value per day, but its periods start at 00:00 local time of the area that published it, whatever that area’s UTC offset is, in every zone. In CET (Warsaw) that instant is 00:00 UTC+2 in summer and 23:00 UTC the day before in winter. Without correction a later ResampleTransformer would give each winter value to the wrong hours. Further east it is a different offset again: 00:00 EEST (UTC+3) in Helsinki in summer, 00:00 UTC+3 all year in a zone with a fixed offset (e.g. Europe/Istanbul).
The rule TimezoneTransformer applies:
A value at timestamp t belongs to the calendar date of t in
daily_anchor. It is moved to 00:00 local time (target_tz) on that date.
The default anchor (daily_anchor=None) is target_tz itself, which is right for A31 data in every zone, because that is where ENTSO-E actually anchors it:
| Zone | A31 period start, in local time | Moved to |
|---|---|---|
| Europe/Warsaw, winter | 23:00 on D-1 (UTC) → already 00:00 CET on D | 00:00 on D (no-op) |
| Europe/Warsaw, summer | 22:00 on D-1 (UTC) → already 00:00 CEST on D | 00:00 on D (no-op) |
| Europe/Helsinki, summer (EEST, UTC+3) | 21:00 on D-1 (UTC) → already 00:00 EEST on D | 00:00 on D (no-op) |
| Europe/Istanbul (fixed UTC+3) | 21:00 on D-1 (UTC) → already 00:00 on D | 00:00 on D (no-op) |
A fixed daily_anchor="UTC+02:00" gets this wrong for any zone whose actual offset isn’t UTC+2 at that moment: it silently misplaces every value in EET/EEST zones (FI, EE, LV, LT, BG, GR) every summer, and every value, year-round, in a zone with a fixed UTC+3 offset. Pass an explicit daily_anchor only when a daily column’s real anchor genuinely differs from target_tz, for example a source that anchors to a fixed offset regardless of the pipeline’s own zone:
TimezoneTransformer(target_tz="Europe/Warsaw", daily_columns=["my_daily_value"], daily_anchor="UTC+02:00")After the move, ResampleTransformer spreads each daily value over exactly its own local day (23, 24 or 25 hours).
Checks:
- A daily column must hold at most one value per day. If it holds more (typically because
ResampleTransformeralready ran and made it dense),TimezoneTransformerraises aValueErrorinstead of collapsing the column to a few values. - If the local midnight a value belongs to is not in the index, that row is added (other columns NaN). This is routine at the start of a winter frame: the A31 value for the first local day is stamped an hour before the other sources begin. If the missing midnight lies inside the data, the index has a hole there and a
UserWarningis emitted.
Pipeline Order
pipeline = ( DataPipeline(timezone="Europe/Warsaw") .add_source(EntsoeSource(...)) # Output: UTC .add_transformer(TimezoneTransformer()) # target_tz inherits DataPipeline.timezone; creates DST gaps .add_transformer(ResampleTransformer(freq="1h")) # Fills DST gaps .add_transformer(LagTransformer(columns=["price"], lags=[1, 2, 7], freq="day")) # Lags by local calendar day)What happens on DST days
Converting to local time adds and removes nothing: the spring switch day has 23 hourly rows (no 02:00, because that local time doesn’t exist) and the autumn switch day has 25 (02:00 twice). ResampleTransformer keeps these 23 and 25 hours. It does not invent an 02:00 row.
Timezone List
For a complete list of valid timezone names, see: List of tz database time zones
Common Timezones
| Timezone | Description |
|---|---|
"Europe/Warsaw" | Poland (CET/CEST) |
"Europe/Berlin" | Germany (CET/CEST) |
"Europe/Paris" | France (CET/CEST) |
"Europe/London" | UK (GMT/BST) |
"America/New_York" | US Eastern |
"UTC" | Coordinated Universal Time |
Behavior
- Naive index: Localized to UTC, then converted
- Timezone-aware index: Directly converted
# Input: 2024-01-01 00:00:00+00:00 (UTC)# Output: 2024-01-01 01:00:00+01:00 (Warsaw = CET)