Skip to content

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=True too, which only concerns values that are present but unparsable.
  • The checks skip nulls: is_isoweek_series(["2024-W01", None]) is True. A series that is empty or all-null is also True, since it contains nothing that violates the format. A null does not excuse a genuine non-match elsewhere, so ["nope", None] is still False.

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']
  1. The import of SeriesIsoWeek is 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']
  1. The import of SeriesIsoWeek is 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.


  1. Extending pandas and polars