Working with dataframes
The pandas_utils and polars_utils modules provide the same API to work with
pandas.Series and polars.Series/polars.Expr respectively.
Both modules require the corresponding extra to be installed, see installation:
python -m pip install "iso-week-date[pandas]" # or [polars], or [all]
The utilities come in two flavors: functions and extensions. They do the same work, so pick whichever reads better in your codebase. Every output on this page is generated by running the code shown.
Available operations
| Operation | ISO week (YYYY-WNN) | ISO week date (YYYY-WNN-D) |
|---|---|---|
| datetime series to ISO week series | datetime_to_isoweek |
datetime_to_isoweekdate |
| ISO week series to datetime series | isoweek_to_datetime |
isoweekdate_to_datetime |
| check a string series matches the format | is_isoweek_series |
is_isoweekdate_series |
Null handling
A null is missing data, not a malformed value, so both backends follow the usual dataframe convention:
- The conversions propagate nulls: a null in the input stays a null, in the same position, in
the output. This holds with
strict=Truetoo, which only concerns values that are present but unparsable. - The checks skip nulls:
is_isoweek_series(["2024-W01", None])isTrue. A series that is empty or all-null is alsoTrue, since it contains nothing that violates the format. A null does not excuse a genuine non-match elsewhere, so["nope", None]is stillFalse.
Dtype handling
The checks answer on the content of a string column, not on the exact dtype holding it. A
dictionary-encoded column (pandas category, polars Categorical / Enum) is read like any other
string column, and an all-null column needs no explicit string dtype to be recognised as such.
A non-null value that is not a string is malformed data, not missing data, so a column holding
lists, dicts or numbers is False rather than an error.
What the checks accept
is_isoweek_series and is_isoweekdate_series accept exactly the strings
IsoWeek and IsoWeekDate accept: the format and
the calendar. Weeks 01 through 53 are all well-formed, but only long ISO years have a week 53, so
2020-W53 passes and 2023-W53 does not.
That makes a check a usable precondition for a conversion, since the two agree on every input:
import pandas as pd
from iso_week_date.pandas_utils import is_isoweek_series, isoweek_to_datetime
weeks = pd.Series(["2023-W53"]) # 2023 has only 52 ISO weeks
print(is_isoweek_series(weeks))
if is_isoweek_series(weeks):
print(isoweek_to_datetime(weeks).to_list())
else:
print("rejected before the conversion could go wrong")
False
rejected before the conversion could go wrong
Older pandas does not complain about these values
isoweek_to_datetime raises on 2023-W53 only from pandas 3.0 onwards. Earlier versions convert
it without complaint and return 2024-01-01, silently rolling a week that does not exist into the
next ISO year. polars refuses on every supported version. Checking first is what protects you on
the versions that do not.
Functions
The functions approach takes the series/expr as an argument and returns a new series/expr.
from datetime import date
import pandas as pd
from iso_week_date.pandas_utils import (
datetime_to_isoweek,
is_isoweek_series,
isoweek_to_datetime,
)
s_date = pd.Series(pd.date_range(date(2023, 1, 1), date(2023, 1, 10), freq="1D"))
print(datetime_to_isoweek(series=s_date, offset=pd.Timedelta(days=1)).to_list())
['2022-W52', '2022-W52', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W02']
s_iso = pd.Series(["2022-W52", "2023-W01", "2023-W02"])
print(isoweek_to_datetime(series=s_iso, offset=pd.Timedelta(days=1)))
0 2022-12-27
1 2023-01-03
2 2023-01-10
dtype: datetime64[us]
print(is_isoweek_series(s_iso))
print(is_isoweek_series(s_iso + "abc"))
True
False
The polars module mirrors it, here shown with the ISO week date variants:
from datetime import timedelta
import polars as pl
from iso_week_date.polars_utils import (
datetime_to_isoweekdate,
is_isoweekdate_series,
isoweekdate_to_datetime,
)
s_date_pl = pl.date_range(
date(2023, 1, 1), date(2023, 1, 10), interval="1d", eager=True
)
print(datetime_to_isoweekdate(s_date_pl, offset=timedelta(days=1)).to_list())
['2022-W52-6', '2022-W52-7', '2023-W01-1', '2023-W01-2', '2023-W01-3', '2023-W01-4', '2023-W01-5', '2023-W01-6', '2023-W01-7', '2023-W02-1']
s_iso_pl = pl.Series(["2022-W52-1", "2023-W01-2", "2023-W02-7"])
print(isoweekdate_to_datetime(series=s_iso_pl, offset=timedelta(days=1)).to_list())
print(is_isoweekdate_series(s_iso_pl))
[datetime.date(2022, 12, 27), datetime.date(2023, 1, 4), datetime.date(2023, 1, 16)]
True
polars date_range returns an expression by default
pl.date_range(...) builds a polars.Expr. Pass eager=True to get a polars.Series you can call .to_list()
on, as above. Both types work with these helpers, see the note on expressions.
Extensions
The extensions1 approach extends the pandas.Series and polars.Series/polars.Expr classes with new methods,
available through the iwd (isoweekdate) namespace.
The method names are shorter on the namespace
The format checks lose their _series suffix as namespace methods, and take no argument since the series is the
receiver: the function is_isoweek_series(series) becomes series.iwd.is_isoweek().
"Translating" the previous examples to extensions:
from datetime import date
import pandas as pd
from iso_week_date.pandas_utils import SeriesIsoWeek # noqa: F401 (1)
s_date = pd.Series(pd.date_range(date(2023, 1, 1), date(2023, 1, 10), freq="1D"))
print(s_date.iwd.datetime_to_isoweek(offset=pd.Timedelta(days=1)).to_list())
['2022-W52', '2022-W52', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W01', '2023-W02']
-
The import of
SeriesIsoWeekis needed to register the extensions.noqa: F401 is added to avoid the linter(s) warning about the unused import.
s_iso = pd.Series(["2022-W52", "2023-W01", "2023-W02"])
print(s_iso.iwd.isoweek_to_datetime(offset=pd.Timedelta(days=1)))
print(s_iso.iwd.is_isoweek())
print((s_iso + "abc").iwd.is_isoweek())
0 2022-12-27
1 2023-01-03
2 2023-01-10
dtype: datetime64[us]
True
False
from datetime import timedelta
import polars as pl
from iso_week_date.polars_utils import SeriesIsoWeek # noqa: F401 (1)
s_date_pl = pl.date_range(
date(2023, 1, 1), date(2023, 1, 10), interval="1d", eager=True
)
print(s_date_pl.iwd.datetime_to_isoweekdate(offset=timedelta(days=1)).to_list())
['2022-W52-6', '2022-W52-7', '2023-W01-1', '2023-W01-2', '2023-W01-3', '2023-W01-4', '2023-W01-5', '2023-W01-6', '2023-W01-7', '2023-W02-1']
-
The import of
SeriesIsoWeekis needed to register the extensions.noqa: F401 is added to avoid the linter(s) warning about the unused import.
s_iso_pl = pl.Series(["2022-W52-1", "2023-W01-2", "2023-W02-7"])
print(s_iso_pl.iwd.isoweekdate_to_datetime(offset=timedelta(days=1)).to_list())
print(s_iso_pl.iwd.is_isoweekdate())
print((s_iso_pl + "abc").iwd.is_isoweekdate())
[datetime.date(2022, 12, 27), datetime.date(2023, 1, 4), datetime.date(2023, 1, 16)]
True
False
Handling unparsable values
isoweek_to_datetime and isoweekdate_to_datetime raise by default when a value does not match the expected format.
Pass strict=False to get a null in place of each bad value instead:
import pandas as pd
from iso_week_date.pandas_utils import isoweek_to_datetime
messy = pd.Series(["2023-W01", "not-a-week"])
try:
isoweek_to_datetime(messy)
except ValueError as e:
print(f"raised {type(e).__name__} on the unparsable value")
print(isoweek_to_datetime(messy, strict=False).to_list())
raised ValueError on the unparsable value
[Timestamp('2023-01-02 00:00:00'), NaT]
The polars versions behave the same way, raising polars.exceptions.InvalidOperationError when strict=True.
Choosing the day of the week
An ISO week covers seven dates, so converting a YYYY-WNN series to datetimes has to pick one. isoweek_to_datetime
defaults to the first day of the week and takes a weekday argument (1 is Monday, 7 is Sunday) to choose another:
week = pd.Series(["2023-W01"])
print(isoweek_to_datetime(week).to_list())
print(isoweek_to_datetime(week, weekday=3).to_list())
[Timestamp('2023-01-02 00:00:00')]
[Timestamp('2023-01-04 00:00:00')]
isoweekdate_to_datetime needs no such argument: the weekday is already part of each value.
A note on polars expressions
The polars extension is registered on both Series and Expr, which means it can be used in any
polars context where an expression is allowed:
from datetime import date, timedelta
import polars as pl
from iso_week_date.polars_utils import SeriesIsoWeek # noqa: F401
df = pl.DataFrame({"date": [date(2023, 1, 1), date(2023, 1, 8)]}).with_columns(
week=pl.col("date").iwd.datetime_to_isoweek(offset=timedelta(days=1))
)
print(df)
shape: (2, 2)
┌────────────┬──────────┐
│ date ┆ week │
│ --- ┆ --- │
│ date ┆ str │
╞════════════╪══════════╡
│ 2023-01-01 ┆ 2022-W52 │
│ 2023-01-08 ┆ 2023-W01 │
└────────────┴──────────┘
The format checks follow the type of what they are given. A Series is data, so it is evaluated on
the spot and the answer is a bool. An Expr is not data, so the answer is a boolean Expr that
whatever context receives it evaluates:
from iso_week_date.polars_utils import is_isoweek_series
weeks = pl.DataFrame({"week": ["2023-W01", "nope"]})
print(is_isoweek_series(weeks["week"])) # a Series in, a bool out
print(type(is_isoweek_series(pl.col("week"))).__name__) # an Expr in, an Expr out
print(weeks.select(all_valid=is_isoweek_series(pl.col("week"))))
False
Expr
shape: (1, 1)
┌───────────┐
│ all_valid │
│ --- │
│ bool │
╞═══════════╡
│ false │
└───────────┘
An expression result is not a bool
if is_isoweek_series(pl.col("week")): raises TypeError, because the truth value of an Expr
is ambiguous. Evaluate it in a context first, or pass a Series when you want an answer now.
Custom offsets
Every conversion helper takes an offset argument, in days or as a timedelta, to shift the start of the week. See
weeks that do not start on Monday.