Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/user_guide/datetime/DatetimeFeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,59 @@ In the following output we see the resulting dataframe:

As you can see, we do not have the constant features in the transformed dataset.

With polars
-----------

:class:`DatetimeFeatures()` also works with polars dataframes.

.. code:: python

import polars as pl
from feature_engine.datetime import DatetimeFeatures

toy_df = pl.DataFrame({
"id": [1, 2, 3, 4],
"var_date": ["2012-06-21", "1998-02-10", "2010-08-03", "2020-10-31"],
})

dfts = DatetimeFeatures(
features_to_extract=["month", "year", "day_of_week", "days_in_month"],
)

df_transf = dfts.fit_transform(toy_df)

df_transf

We see the new features in the following output:

.. code:: text

shape: (4, 5)
┌─────┬────────────────┬───────────────┬──────────────────────┬────────────────────────┐
│ id ┆ var_date_month ┆ var_date_year ┆ var_date_day_of_week ┆ var_date_days_in_month │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i8 ┆ i32 ┆ i8 ┆ i8 │
╞═════╪════════════════╪═══════════════╪══════════════════════╪════════════════════════╡
│ 1 ┆ 6 ┆ 2012 ┆ 3 ┆ 30 │
│ 2 ┆ 2 ┆ 1998 ┆ 1 ┆ 28 │
│ 3 ┆ 8 ┆ 2010 ┆ 1 ┆ 31 │
│ 4 ┆ 10 ┆ 2020 ┆ 5 ┆ 31 │
└─────┴────────────────┴───────────────┴──────────────────────┴────────────────────────┘

.. note::

When parsing string columns, pandas relies on `dateutil` and can infer loose or
ambiguous formats. Polars parses dates natively and needs the format to be
unambiguous and consistent across the column: ISO 8601 (e.g. *2012-06-21*) parses
reliably, but looser formats, such as day-first dates like *21/06/2012*, require an
explicit `format`. The `dayfirst`, `yearfirst` and `utc` parameters are
`pandas.to_datetime`-only options and have no effect on polars input.

.. note::

`variables="index"` is only supported when `X` is a pandas dataframe, since only pandas
dataframes have an index.

Working with different timezones
--------------------------------

Expand Down
103 changes: 103 additions & 0 deletions feature_engine/datetime/_datetime_constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import narwhals as nw
import numpy as np

FEATURES_SUPPORTED = [
Expand Down Expand Up @@ -78,3 +79,105 @@
"minute": lambda x: x.dt.minute,
"second": lambda x: x.dt.second,
}


def _nw_quarter(x: nw.Series) -> nw.Series:
return ((x.dt.month() - 1) // 3) + 1


def _nw_semester(x: nw.Series) -> nw.Series:
return (x.dt.month() > 6).cast(nw.Int64()) + 1


def _nw_week(x: nw.Series) -> nw.Series:
# narwhals has no isocalendar(); the "%V" strftime code (ISO week) round-trips
# correctly on every backend tested (pandas, polars) via to_string().
return x.dt.to_string("%V").cast(nw.Int64())


def _nw_day_of_week(x: nw.Series) -> nw.Series:
# narwhals weekday() is 1=Monday..7=Sunday; pandas dayofweek is 0=Monday..6=Sunday.
return x.dt.weekday() - 1


def _nw_weekend(x: nw.Series) -> nw.Series:
return (_nw_day_of_week(x) >= 5).cast(nw.Int64())


def _nw_is_month_start(x: nw.Series) -> nw.Series:
return x.dt.day() == 1


def _nw_is_month_end(x: nw.Series) -> nw.Series:
# no days_in_month()/is_month_end() in narwhals: a day belongs to the last
# day of its month iff the next day rolls over into a different month.
return x.dt.offset_by("1d").dt.month() != x.dt.month()


def _nw_month_start(x: nw.Series) -> nw.Series:
return _nw_is_month_start(x).cast(nw.Int64())


def _nw_month_end(x: nw.Series) -> nw.Series:
return _nw_is_month_end(x).cast(nw.Int64())


def _nw_quarter_start(x: nw.Series) -> nw.Series:
# quarters start in Jan/Apr/Jul/Oct, the only months where month % 3 == 1.
return (_nw_is_month_start(x) & (x.dt.month() % 3 == 1)).cast(nw.Int64())


def _nw_quarter_end(x: nw.Series) -> nw.Series:
# quarters end in Mar/Jun/Sep/Dec, the only months where month % 3 == 0.
return (_nw_is_month_end(x) & (x.dt.month() % 3 == 0)).cast(nw.Int64())


def _nw_year_start(x: nw.Series) -> nw.Series:
return (_nw_is_month_start(x) & (x.dt.month() == 1)).cast(nw.Int64())


def _nw_year_end(x: nw.Series) -> nw.Series:
return (_nw_is_month_end(x) & (x.dt.month() == 12)).cast(nw.Int64())


def _nw_leap_year(x: nw.Series) -> nw.Series:
year = x.dt.year()
return (((year % 4 == 0) & (year % 100 != 0)) | (year % 400 == 0)).cast(
nw.Int64()
)


def _nw_days_in_month(x: nw.Series) -> nw.Series:
# start of month, plus a month, minus a day = last day of the original month;
# its day number is the month's length. Handles leap years automatically.
return x.dt.truncate("1mo").dt.offset_by("1mo").dt.offset_by("-1d").dt.day()


# narwhals-native equivalents of FEATURES_FUNCTIONS above, used for dataframe
# backends other than pandas. Kept separate from FEATURES_FUNCTIONS because
# roughly a third of these features (week, month_end, quarter_end, quarter_start,
# year_start, year_end, leap_year, days_in_month) benchmarked 2x-53x slower than
# pandas-native when run through narwhals on a pandas backend, so pandas keeps its
# fast, unchanged native path.
FEATURES_FUNCTIONS_NARWHALS = {
"month": lambda x: x.dt.month(),
"quarter": _nw_quarter,
"semester": _nw_semester,
"year": lambda x: x.dt.year(),
"week": _nw_week,
"day_of_week": _nw_day_of_week,
"day_of_month": lambda x: x.dt.day(),
"day_of_year": lambda x: x.dt.ordinal_day(),
"weekend": _nw_weekend,
"month_start": _nw_month_start,
"month_end": _nw_month_end,
"quarter_start": _nw_quarter_start,
"quarter_end": _nw_quarter_end,
"year_start": _nw_year_start,
"year_end": _nw_year_end,
"leap_year": _nw_leap_year,
"days_in_month": _nw_days_in_month,
"hour": lambda x: x.dt.hour(),
"minute": lambda x: x.dt.minute(),
"second": lambda x: x.dt.second(),
}
Loading