eb-features 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Feature engineering utilities for the Electric Barometer ecosystem.
5
+
6
+ This package provides modular, domain-agnostic feature engineering components
7
+ used across forecasting, evaluation, and operational modeling workflows.
8
+
9
+ Subpackages
10
+ -----------
11
+ panel
12
+ Feature engineering utilities for panel (entity × timestamp) time-series data.
13
+
14
+ Notes
15
+ -----
16
+ This package intentionally exposes a small public API. Most functionality is
17
+ accessed through subpackages such as `eb_features.panel`.
18
+ """
19
+
20
+ __all__ = []
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Panel time-series feature engineering.
5
+
6
+ This subpackage provides lightweight, frequency-agnostic feature engineering utilities
7
+ for *panel* time-series data, i.e., data indexed by:
8
+
9
+ - an **entity** identifier (store, SKU, customer, sensor, etc.), and
10
+ - a **timestamp** column
11
+
12
+ The primary public interface is:
13
+
14
+ - [`FeatureConfig`][eb_features.panel.engineering.FeatureConfig]: declarative feature configuration
15
+ - [`FeatureEngineer`][eb_features.panel.engineering.FeatureEngineer]: stateless transformer that
16
+ produces a model-ready ``(X, y, feature_names)`` triple from a long-form DataFrame.
17
+
18
+ Design goals
19
+ ------------
20
+ - **Stateless**: no fit/transform lifecycle; features are generated deterministically from inputs.
21
+ - **Frequency-agnostic**: lags and rolling windows are expressed in index steps, not wall-clock units.
22
+ - **Leakage-aware**: feature construction is intended to use only information available at or
23
+ before prediction time (depending on configuration and implementation details).
24
+
25
+ Notes
26
+ -----
27
+ This subpackage is intentionally conservative in scope. It focuses on producing features suitable
28
+ for classical supervised learning pipelines (tree models, linear models, shallow neural nets) that
29
+ expect a fixed-width design matrix.
30
+
31
+ See Also
32
+ --------
33
+ - [`eb_features.panel.engineering.FeatureEngineer`][eb_features.panel.engineering.FeatureEngineer]
34
+ - [`eb_features.panel.engineering.FeatureConfig`][eb_features.panel.engineering.FeatureConfig]
35
+ """
36
+
37
+ from eb_features.panel.engineering import FeatureConfig, FeatureEngineer
38
+
39
+ __all__ = [
40
+ "FeatureConfig",
41
+ "FeatureEngineer",
42
+ ]
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Calendar and time-derived features for panel time series.
5
+
6
+ This module provides utilities to derive calendar/time features from a timestamp column.
7
+ It is designed for *panel* time-series data (entity × timestamp) and is typically used
8
+ as part of a broader feature engineering pipeline.
9
+
10
+ Supported base calendar features
11
+ --------------------------------
12
+ Given a timestamp column ``timestamp_col``:
13
+
14
+ - ``"hour"``: Hour of day in ``[0, 23]``
15
+ - ``"dow"``: Day of week in ``[0, 6]`` where Monday=0 (pandas convention)
16
+ - ``"dom"``: Day of month in ``[1, 31]``
17
+ - ``"month"``: Month in ``[1, 12]``
18
+ - ``"is_weekend"``: Weekend indicator (Saturday/Sunday) as ``0/1``
19
+
20
+ Optional cyclical encodings
21
+ ---------------------------
22
+ Certain calendar attributes are periodic and can be represented with sine/cosine pairs:
23
+
24
+ - hour (period 24):
25
+
26
+ $$
27
+ \sin\left(2\pi \frac{\mathrm{hour}}{24}\right),\quad
28
+ \cos\left(2\pi \frac{\mathrm{hour}}{24}\right)
29
+ $$
30
+
31
+ - day of week (period 7):
32
+
33
+ $$
34
+ \sin\left(2\pi \frac{\mathrm{dow}}{7}\right),\quad
35
+ \cos\left(2\pi \frac{\mathrm{dow}}{7}\right)
36
+ $$
37
+
38
+ These encodings are added only when:
39
+ - the corresponding base feature (hour or dow) is present, and
40
+ - ``use_cyclical_time=True``.
41
+
42
+ Notes
43
+ -----
44
+ - Feature construction is *stateless*: functions operate only on the provided DataFrame.
45
+ - Time features are derived from ``pandas.to_datetime`` conversion of ``timestamp_col``.
46
+ Timezone-aware timestamps are supported; feature values reflect the timestamp's local
47
+ representation as stored in the column.
48
+ """
49
+
50
+ from typing import Iterable, List, Sequence, Tuple
51
+
52
+ import numpy as np
53
+ import pandas as pd
54
+
55
+ from eb_features.panel.constants import (
56
+ ALLOWED_CALENDAR_FEATURES,
57
+ DOW_PERIOD,
58
+ HOUR_PERIOD,
59
+ WEEKEND_DAYS,
60
+ )
61
+
62
+
63
+ def add_calendar_features(
64
+ df: pd.DataFrame,
65
+ *,
66
+ timestamp_col: str,
67
+ calendar_features: Sequence[str],
68
+ use_cyclical_time: bool = True,
69
+ ) -> Tuple[pd.DataFrame, List[str], List[str]]:
70
+ r"""
71
+ Add calendar/time-derived features to a DataFrame.
72
+
73
+ Parameters
74
+ ----------
75
+ df : pandas.DataFrame
76
+ Input DataFrame containing a timestamp column.
77
+ timestamp_col : str
78
+ Name of the timestamp column.
79
+ calendar_features : Sequence[str]
80
+ Base calendar features to derive. Allowed values are:
81
+ ``{"hour", "dow", "dom", "month", "is_weekend"}``.
82
+ use_cyclical_time : bool, default True
83
+ If True, add sine/cosine encodings for hour and/or day-of-week when those
84
+ base features are included.
85
+
86
+ Returns
87
+ -------
88
+ df_out : pandas.DataFrame
89
+ Copy of ``df`` with the requested calendar features added as columns.
90
+ feature_cols : list[str]
91
+ Names of all features added by this call, including cyclical encodings (if any).
92
+ calendar_cols : list[str]
93
+ Names of base calendar feature columns added (excludes cyclical encodings).
94
+
95
+ Raises
96
+ ------
97
+ KeyError
98
+ If ``timestamp_col`` is not present in ``df``.
99
+ ValueError
100
+ If an unsupported calendar feature is requested.
101
+
102
+ Notes
103
+ -----
104
+ The derived columns are currently named:
105
+
106
+ - ``hour``
107
+ - ``dayofweek`` (for requested ``"dow"``)
108
+ - ``dayofmonth`` (for requested ``"dom"``)
109
+ - ``month``
110
+ - ``is_weekend``
111
+
112
+ This naming is stable and intended to be referenced by downstream steps.
113
+ """
114
+ if timestamp_col not in df.columns:
115
+ raise KeyError(f"Timestamp column {timestamp_col!r} not found in DataFrame.")
116
+
117
+ _validate_calendar_features(calendar_features)
118
+
119
+ df_out = df.copy()
120
+ ts = pd.to_datetime(df_out[timestamp_col])
121
+
122
+ calendar_cols: List[str] = []
123
+ feature_cols: List[str] = []
124
+
125
+ for name in calendar_features:
126
+ if name == "hour":
127
+ col = "hour"
128
+ df_out[col] = ts.dt.hour.astype("int16")
129
+ calendar_cols.append(col)
130
+ elif name == "dow":
131
+ col = "dayofweek"
132
+ df_out[col] = ts.dt.dayofweek.astype("int16")
133
+ calendar_cols.append(col)
134
+ elif name == "dom":
135
+ col = "dayofmonth"
136
+ df_out[col] = ts.dt.day.astype("int16")
137
+ calendar_cols.append(col)
138
+ elif name == "month":
139
+ col = "month"
140
+ df_out[col] = ts.dt.month.astype("int16")
141
+ calendar_cols.append(col)
142
+ elif name == "is_weekend":
143
+ col = "is_weekend"
144
+ df_out[col] = ts.dt.dayofweek.isin(WEEKEND_DAYS).astype("int8")
145
+ calendar_cols.append(col)
146
+ else: # pragma: no cover
147
+ # Guarded by _validate_calendar_features, but keep a defensive fallback.
148
+ raise ValueError(
149
+ f"Unsupported calendar feature {name!r}. Allowed: {sorted(ALLOWED_CALENDAR_FEATURES)}."
150
+ )
151
+
152
+ feature_cols.extend(calendar_cols)
153
+
154
+ # ---------------------------------------------------------------------
155
+ # Optional cyclical encodings
156
+ # ---------------------------------------------------------------------
157
+ if use_cyclical_time:
158
+ if "hour" in calendar_cols:
159
+ hour = df_out["hour"].astype(float)
160
+ df_out["hour_sin"] = np.sin(2.0 * np.pi * hour / float(HOUR_PERIOD))
161
+ df_out["hour_cos"] = np.cos(2.0 * np.pi * hour / float(HOUR_PERIOD))
162
+ feature_cols.extend(["hour_sin", "hour_cos"])
163
+
164
+ if "dayofweek" in calendar_cols:
165
+ dow = df_out["dayofweek"].astype(float)
166
+ df_out["dow_sin"] = np.sin(2.0 * np.pi * dow / float(DOW_PERIOD))
167
+ df_out["dow_cos"] = np.cos(2.0 * np.pi * dow / float(DOW_PERIOD))
168
+ feature_cols.extend(["dow_sin", "dow_cos"])
169
+
170
+ return df_out, feature_cols, calendar_cols
171
+
172
+
173
+ def _validate_calendar_features(calendar_features: Iterable[str]) -> None:
174
+ r"""
175
+ Validate that requested calendar features are supported.
176
+
177
+ Parameters
178
+ ----------
179
+ calendar_features : Iterable[str]
180
+ Candidate calendar feature keys.
181
+
182
+ Raises
183
+ ------
184
+ ValueError
185
+ If any feature key is unsupported.
186
+ """
187
+ requested = list(calendar_features)
188
+ invalid = [f for f in requested if f not in ALLOWED_CALENDAR_FEATURES]
189
+ if invalid:
190
+ raise ValueError(
191
+ "Unsupported calendar feature(s) requested: "
192
+ f"{invalid}. Allowed: {sorted(ALLOWED_CALENDAR_FEATURES)}."
193
+ )
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Configuration objects for panel time-series feature engineering.
5
+
6
+ This module defines declarative configuration used by the panel feature engineering
7
+ pipeline. The configuration is designed to be:
8
+
9
+ - **frequency-agnostic** (lags/windows are expressed in index steps, not wall-clock time)
10
+ - **stateless** (config describes what to compute; no fitted state is stored)
11
+ - **explicit** (feature families are enabled/disabled via lists/flags)
12
+
13
+ Notes
14
+ -----
15
+ This configuration is consumed by [`FeatureEngineer`][eb_features.panel.engineer.FeatureEngineer]
16
+ and related helper modules (lags, rolling, calendar, encoders).
17
+ """
18
+
19
+ from dataclasses import dataclass, field
20
+ from typing import Optional, Sequence, Tuple
21
+
22
+ # -------------------------------------------------------------------------
23
+ # Public constants (useful for validation, docs, and IDE discoverability)
24
+ # -------------------------------------------------------------------------
25
+
26
+ #: Allowed rolling statistics for rolling window features.
27
+ ALLOWED_ROLLING_STATS: Tuple[str, ...] = ("mean", "std", "min", "max", "sum", "median")
28
+
29
+ #: Allowed calendar feature keys derived from timestamps.
30
+ ALLOWED_CALENDAR_FEATURES: Tuple[str, ...] = ("hour", "dow", "dom", "month", "is_weekend")
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class FeatureConfig:
35
+ r"""
36
+ Configuration for panel time-series feature engineering.
37
+
38
+ The feature engineering pipeline assumes a long-form panel DataFrame with an entity
39
+ identifier column, a timestamp column, and a numeric target column. This configuration
40
+ describes which feature families to generate and which passthrough columns to include.
41
+
42
+ Notes
43
+ -----
44
+ - Lag steps and rolling windows are expressed in **index steps** (rows) at the input
45
+ frequency, not in wall-clock units.
46
+ - Lags and rolling windows are computed **within each entity**.
47
+ - If ``dropna=True``, rows lacking sufficient lag/rolling history are dropped after
48
+ feature construction.
49
+
50
+ Attributes
51
+ ----------
52
+ lag_steps : Sequence[int] | None
53
+ Positive lag offsets (in steps) applied to the target. For each ``k`` in ``lag_steps``,
54
+ the feature ``lag_{k}`` is added.
55
+ rolling_windows : Sequence[int] | None
56
+ Positive rolling window lengths (in steps) applied to the target. For each ``w`` in
57
+ ``rolling_windows`` and each stat in ``rolling_stats``, the feature
58
+ ``roll_{w}_{stat}`` is added.
59
+ rolling_stats : Sequence[str]
60
+ Rolling statistics to compute. Allowed values are:
61
+ ``{"mean", "std", "min", "max", "sum", "median"}``.
62
+ calendar_features : Sequence[str]
63
+ Calendar features derived from the timestamp column. Allowed values are:
64
+ ``{"hour", "dow", "dom", "month", "is_weekend"}``.
65
+ use_cyclical_time : bool
66
+ If True, add sine/cosine encodings for hour and day-of-week when those base
67
+ calendar columns are present.
68
+ regressor_cols : Sequence[str] | None
69
+ Numeric external regressors to pass through. If None, numeric columns may be
70
+ auto-detected by the calling pipeline (excluding entity/timestamp/target and
71
+ ``static_cols``).
72
+ static_cols : Sequence[str] | None
73
+ Entity-level metadata columns already present on the input DataFrame. These are
74
+ passed through directly as features.
75
+ dropna : bool
76
+ If True, drop rows with NaNs in any engineered feature columns (typically caused
77
+ by lags and rolling windows).
78
+ """
79
+
80
+ lag_steps: Optional[Sequence[int]] = field(default_factory=lambda: (1, 2, 24))
81
+
82
+ rolling_windows: Optional[Sequence[int]] = field(default_factory=lambda: (3, 24))
83
+ rolling_stats: Sequence[str] = field(
84
+ default_factory=lambda: ("mean", "std", "min", "max", "sum")
85
+ )
86
+
87
+ calendar_features: Sequence[str] = field(
88
+ default_factory=lambda: ("hour", "dow", "month", "is_weekend")
89
+ )
90
+ use_cyclical_time: bool = True
91
+
92
+ regressor_cols: Optional[Sequence[str]] = None
93
+ static_cols: Optional[Sequence[str]] = None
94
+
95
+ dropna: bool = True
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Shared constants for panel feature engineering.
5
+
6
+ This module centralizes small, stable configuration values used across the
7
+ ``eb_features.panel`` subpackage. Keeping these definitions in one place prevents
8
+ validation drift between modules and provides a single reference point for both
9
+ implementation and documentation.
10
+
11
+ Notes
12
+ -----
13
+ - These constants are intentionally minimal and low-churn.
14
+ - They define *allowed values*, *default configurations*, and *calendar parameters*
15
+ used consistently across feature builders.
16
+ """
17
+
18
+ from typing import Final, FrozenSet, Tuple
19
+
20
+ # -----------------------------------------------------------------------------
21
+ # Allowed feature keys
22
+ # -----------------------------------------------------------------------------
23
+ ALLOWED_ROLLING_STATS: FrozenSet[str] = frozenset(
24
+ {"mean", "std", "min", "max", "sum", "median"}
25
+ )
26
+ """
27
+ Allowed rolling-window summary statistics.
28
+
29
+ Each statistic corresponds to a feature name of the form:
30
+
31
+ $$
32
+ \mathrm{roll\_{w}\_{stat}}(t)
33
+ $$
34
+
35
+ where ``w`` is the window length (in index steps) and ``stat`` is one of the allowed values.
36
+ """
37
+
38
+
39
+ ALLOWED_CALENDAR_FEATURES: FrozenSet[str] = frozenset(
40
+ {"hour", "dow", "dom", "month", "is_weekend"}
41
+ )
42
+ """
43
+ Allowed calendar features derived from the timestamp column.
44
+
45
+ Calendar features are added as integer-valued columns and may optionally be accompanied by
46
+ cyclical encodings (sine/cosine) for periodic components.
47
+ """
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # Default configuration values (used by FeatureConfig)
51
+ # -----------------------------------------------------------------------------
52
+ DEFAULT_LAG_STEPS: Final[Tuple[int, ...]] = (1, 2, 24)
53
+
54
+ DEFAULT_ROLLING_WINDOWS: Final[Tuple[int, ...]] = (3, 24)
55
+ DEFAULT_ROLLING_STATS: Final[Tuple[str, ...]] = ("mean", "std", "min", "max", "sum")
56
+
57
+ DEFAULT_CALENDAR_FEATURES: Final[Tuple[str, ...]] = (
58
+ "hour",
59
+ "dow",
60
+ "month",
61
+ "is_weekend",
62
+ )
63
+
64
+ # -----------------------------------------------------------------------------
65
+ # Calendar / cyclical encoding parameters
66
+ # -----------------------------------------------------------------------------
67
+ HOUR_PERIOD: Final[int] = 24
68
+ """Period used for cyclical hour-of-day encodings."""
69
+
70
+ DOW_PERIOD: Final[int] = 7
71
+ """Period used for cyclical day-of-week encodings."""
72
+
73
+ # pandas dt.dayofweek convention: Monday=0 ... Sunday=6
74
+ WEEKEND_DAYS: Final[Tuple[int, int]] = (5, 6)
75
+ """Day-of-week values corresponding to Saturday and Sunday."""
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Encoding utilities for panel feature engineering.
5
+
6
+ This module provides small, stateless helpers to make feature matrices numeric.
7
+
8
+ Current scope
9
+ -------------
10
+ The panel feature engineering pipeline produces a feature frame that may contain
11
+ a mixture of numeric and non-numeric columns (e.g., entity metadata strings).
12
+ Many downstream estimators expect purely numeric arrays. The helper in this module
13
+ encodes non-numeric columns using pandas categorical codes.
14
+
15
+ Important
16
+ ---------
17
+ Categorical codes are stable **only within the provided DataFrame**. Because this
18
+ module is intentionally stateless (no fitted mapping is persisted), codes may differ
19
+ between training and inference if category sets or ordering differ.
20
+
21
+ For production modeling pipelines that require consistent encodings across datasets,
22
+ consider:
23
+ - pre-encoding categoricals upstream (one-hot, target encoding, etc.), or
24
+ - introducing a fitted encoder with persisted category mappings.
25
+ """
26
+
27
+ from typing import Iterable
28
+
29
+ import pandas as pd
30
+ from pandas.api.types import is_bool_dtype, is_numeric_dtype
31
+
32
+
33
+ def encode_non_numeric_as_category_codes(
34
+ feature_frame: pd.DataFrame,
35
+ *,
36
+ columns: Iterable[str] | None = None,
37
+ dtype: str = "int32",
38
+ ) -> pd.DataFrame:
39
+ """
40
+ Encode non-numeric feature columns as categorical codes.
41
+
42
+ Parameters
43
+ ----------
44
+ feature_frame : pandas.DataFrame
45
+ Feature DataFrame whose columns will be encoded as needed.
46
+ columns : Iterable[str] | None, default None
47
+ Columns to consider for encoding. If None, all columns are considered.
48
+ dtype : str, default "int32"
49
+ Output dtype for encoded columns. (Booleans are converted to 0/1 and cast
50
+ to this dtype; categorical codes are integers cast to this dtype.)
51
+
52
+ Returns
53
+ -------
54
+ pandas.DataFrame
55
+ Copy of ``feature_frame`` where:
56
+ - boolean columns are converted to {0,1}
57
+ - non-numeric, non-boolean columns are replaced by categorical integer codes
58
+
59
+ Notes
60
+ -----
61
+ - Missing values in non-numeric columns are assigned the code ``-1`` by pandas.
62
+ - Category ordering is made deterministic by sorting observed values by their
63
+ string representation before assigning codes.
64
+ """
65
+ df = feature_frame.copy()
66
+ selected = list(df.columns) if columns is None else list(columns)
67
+
68
+ missing = [c for c in selected if c not in df.columns]
69
+ if missing:
70
+ raise KeyError(f"Columns not found in feature_frame: {missing}.")
71
+
72
+ for col in selected:
73
+ s = df[col]
74
+
75
+ # Convert booleans to 0/1 (numeric), for a clean numeric feature matrix.
76
+ if is_bool_dtype(s):
77
+ df[col] = s.astype("int8").astype(dtype)
78
+ continue
79
+
80
+ # Leave numeric columns unchanged.
81
+ if is_numeric_dtype(s):
82
+ continue
83
+
84
+ # Deterministic category ordering for a given input DataFrame.
85
+ observed = s.dropna().unique().tolist()
86
+ categories = sorted(observed, key=lambda x: str(x))
87
+
88
+ cat = pd.Categorical(s, categories=categories, ordered=False)
89
+ df[col] = cat.codes.astype(dtype)
90
+
91
+ return df