Skip to content
Open
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
54 changes: 54 additions & 0 deletions docs/user_guide/datetime/DatetimeFeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,60 @@ 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, and with any other
dataframe library supported by `narwhals <https://narwhals-dev.github.io/narwhals/>`_.

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

For non-pandas input, string columns are parsed with narwhals'
`Series.str.to_datetime() <https://narwhals-dev.github.io/narwhals/api-reference/series_str/#narwhals.series.SeriesStringNamespace.to_datetime>`_,
which can only infer unambiguous formats, like ISO-8601. For anything else, e.g. day-first
dates such as *21/06/2012*, pass an explicit `format`. The `dayfirst`, `yearfirst` and
`utc` parameters are pandas-`to_datetime`-only options and have no effect on non-pandas
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 (rather than
# merged into one dispatch) 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