[Python] Performing integer-based rolling window group_by using Python Polars

Discussão em 'Python' iniciado por Stack, Setembro 25, 2024.

  1. Stack

    Stack Membro Participativo

    I have a outer/inner loop-based function I'm trying to vectorise using Python Polars DataFrames. The function is a type of moving average and will be used to filter time-series financial data. Here's the function:

    def ma_j(df_src: pl.DataFrame, depth: float):

    jrc04 = 0.0
    jrc05 = 0.0
    jrc06 = 0.0
    jrc08 = 0.0

    series = df_src['close']

    for x in range(0, len(series)):
    if x >= x - depth*2:
    for k in np.arange(start=math.ceil(depth), stop=0, step=-1):
    jrc04 = jrc04 + abs(series[x-k] - series[x-(k+1)])
    jrc05 = jrc05 + (depth + k) * abs(series[x-k] - series[x-(k+1)])
    jrc06 = jrc06 + series[x-(k+1)]
    else:
    jrc03 = abs(series - (series[1]))
    jrc13 = abs(series[x-depth] - series[x - (depth+1)])
    jrc04 = jrc04 - jrc13 + jrc03
    jrc05 = jrc05 - jrc04 + jrc03 * depth
    jrc06 = jrc06 - series[x - (depth+1)] + series[x-1]
    jrc08 = abs(depth * series[x] - jrc06)

    if jrc05 == 0.0:
    ma = 0.0
    else:
    ma = jrc08/jrc05

    return ma


    The tricky bit for me are multiple the inner loop look-backs (for k in...). I've looked through multiple examples that use group_by_dynamic on the timeseries data. For example, here. I've also seen an example for rolling, but this still seems to use a period.

    However, I'd like to strip away the timeseries and just use source Series. Does this mean I need to group on an integer range?

    Using this data example:

    import polars as pl
    import numpy as np

    i, t, v = np.arange(0, 50, 1), np.arange(0, 100, 2), np.random.randint(1,101,50)
    df = pl.DataFrame({"i": i, "t": t, "rand": v})
    df = df.with_columns((pl.datetime(2022,10,30) + pl.duration(seconds=df["t"])).alias("datetime")).drop("t")
    cols = ["i", "datetime", "rand"]
    df = df.select(cols)


    DataFrame looks like this:

    shape: (50, 3)
    ┌─────┬─────────────────────┬──────┐
    │ i ┆ datetime ┆ rand │
    │ --- ┆ --- ┆ --- │
    │ i64 ┆ datetime[μs] ┆ i64 │
    ╞═════╪═════════════════════╪══════╡
    │ 0 ┆ 2022-10-30 00:00:00 ┆ 87 │
    │ 1 ┆ 2022-10-30 00:00:02 ┆ 66 │
    │ 2 ┆ 2022-10-30 00:00:04 ┆ 30 │
    │ 3 ┆ 2022-10-30 00:00:06 ┆ 87 │
    │ 4 ┆ 2022-10-30 00:00:08 ┆ 74 │
    │ … ┆ … ┆ … │
    │ 45 ┆ 2022-10-30 00:01:30 ┆ 91 │
    │ 46 ┆ 2022-10-30 00:01:32 ┆ 52 │
    │ 47 ┆ 2022-10-30 00:01:34 ┆ 68 │
    │ 48 ┆ 2022-10-30 00:01:36 ┆ 26 │
    │ 49 ┆ 2022-10-30 00:01:38 ┆ 99 │
    └─────┴─────────────────────┴──────┘


    ...I can do a grouping by datetime like this":

    df.group_by_dynamic("datetime", every="10s").agg(
    pl.col("rand").mean().alias('rolling mean')
    )


    which gives this: [​IMG]

    But there's 3 issues with this:

    • I don't want to group of datetime...I want to group on every row (maybe i?) in bins of [x] size.
    • I need values against every row
    • I would like to define the aggregation function, as per the various cases in the function above

    Any tips on how I could attack this using Polars? Thanks.

    ---------- Edit 1


    Following @ritchie46 's awesome advice (thanks mate!), here's the groupby:

    result_grp = (
    df
    .rolling(index_column="i", period="10i")
    .agg(
    pl.len().alias("rolling_slots"),
    pl.col("rand").mean().alias("roll_mean")
    )
    )

    df2 = df.select(
    pl.all(),
    result_grp.get_column("rolling_slots"),
    result_grp.get_column("roll_mean"),
    )


    This now gives:

    shape: (50, 5)
    ┌─────┬─────────────────────┬──────┬───────────────┬───────────┐
    │ i ┆ datetime ┆ rand ┆ rolling_slots ┆ roll_mean │
    │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
    │ i64 ┆ datetime[μs] ┆ i64 ┆ u32 ┆ f64 │
    ╞═════╪═════════════════════╪══════╪═══════════════╪═══════════╡
    │ 0 ┆ 2022-10-30 00:00:00 ┆ 23 ┆ 1 ┆ 23.0 │
    │ 1 ┆ 2022-10-30 00:00:02 ┆ 72 ┆ 2 ┆ 47.5 │
    │ 2 ┆ 2022-10-30 00:00:04 ┆ 46 ┆ 3 ┆ 47.0 │
    │ 3 ┆ 2022-10-30 00:00:06 ┆ 37 ┆ 4 ┆ 44.5 │
    │ 4 ┆ 2022-10-30 00:00:08 ┆ 12 ┆ 5 ┆ 38.0 │
    │ … ┆ … ┆ … ┆ … ┆ … │
    │ 45 ┆ 2022-10-30 00:01:30 ┆ 95 ┆ 10 ┆ 53.7 │
    │ 46 ┆ 2022-10-30 00:01:32 ┆ 100 ┆ 10 ┆ 62.7 │
    │ 47 ┆ 2022-10-30 00:01:34 ┆ 6 ┆ 10 ┆ 62.2 │
    │ 48 ┆ 2022-10-30 00:01:36 ┆ 27 ┆ 10 ┆ 56.5 │
    │ 49 ┆ 2022-10-30 00:01:38 ┆ 33 ┆ 10 ┆ 54.5 │
    └─────┴─────────────────────┴──────┴───────────────┴───────────┘


    This is great; now instead of mean(), how do I apply a custom function on the grouped values, such as:

    f_jparams(depth_array, jrc04, jrc05, jrc06, jrc08):

    _depth = len(depth_array)

    if len(depth_array) > 3:
    for x in np.arange(start=1, stop=len(depth_array), step=1):
    jrc04 = jrc04 + abs(depth_array[x] - depth_array[x-1])
    jrc05 = jrc05 + (_depth+x) * abs(depth_array[x] - depth_array[x-1])
    jrc06 = jrc06 + depth_array[x-1]
    else:
    jrc03 = abs(depth_array[_depth-1] - depth_array[_depth-2])
    jrc13 = abs(depth_array[0] - depth_array[1])
    jrc04 = jrc04 - jrc13 + jrc03
    jrc05 = jrc05 - jrc04 + jrc03*_depth
    jrc06 = jrc06 - depth_array[1] + depth_array[_depth-2]

    jrc08 = abs(_depth * depth_array[0] - jrc06)

    if jrc05 == 0.0:
    ma = 0.0
    else:
    ma = jrc08/jrc05

    return ma, jrc04, jrc05, jrc06, jrc08


    Thanks!

    ---- Edit 2:


    Thanks to this post, I can collect up the items in the rand rolling group into a list for each row:

    depth = 10

    result_grp = (
    df
    .rolling(
    index_column="i",
    period=str(depth) + "i",
    # offset="0i",
    # closed="left"
    )
    .agg(
    pl.len().alias("rolling_slots"),
    pl.col("rand").mean().alias("roll_mean"),
    pl.col("rand").name.suffix('_val_list'),
    )
    )

    df2 = df.select(
    pl.all(),
    result_grp.get_column("rolling_slots"),
    result_grp.get_column("roll_mean"),
    result_grp.get_column("rand_val_list"),
    )


    Also from this post, I saw a way to make the rolling window period a variable; nice!

    Is there a way to use get_columns and exclude together so I don't have to list every col I want?

    The dataframe now looks like:

    shape: (50, 6)
    ┌─────┬─────────────────────┬──────┬───────────────┬───────────┬─────────────────┐
    │ i ┆ datetime ┆ rand ┆ rolling_slots ┆ roll_mean ┆ rand_val_list │
    │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
    │ i64 ┆ datetime[μs] ┆ i64 ┆ u32 ┆ f64 ┆ list[i64] │
    ╞═════╪═════════════════════╪══════╪═══════════════╪═══════════╪═════════════════╡
    │ 0 ┆ 2022-10-30 00:00:00 ┆ 23 ┆ 1 ┆ 23.0 ┆ [23] │
    │ 1 ┆ 2022-10-30 00:00:02 ┆ 72 ┆ 2 ┆ 47.5 ┆ [23, 72] │
    │ 2 ┆ 2022-10-30 00:00:04 ┆ 46 ┆ 3 ┆ 47.0 ┆ [23, 72, 46] │
    │ 3 ┆ 2022-10-30 00:00:06 ┆ 37 ┆ 4 ┆ 44.5 ┆ [23, 72, … 37] │
    │ 4 ┆ 2022-10-30 00:00:08 ┆ 12 ┆ 5 ┆ 38.0 ┆ [23, 72, … 12] │
    │ … ┆ … ┆ … ┆ … ┆ … ┆ … │
    │ 45 ┆ 2022-10-30 00:01:30 ┆ 95 ┆ 10 ┆ 53.7 ┆ [10, 11, … 95] │
    │ 46 ┆ 2022-10-30 00:01:32 ┆ 100 ┆ 10 ┆ 62.7 ┆ [11, 84, … 100] │
    │ 47 ┆ 2022-10-30 00:01:34 ┆ 6 ┆ 10 ┆ 62.2 ┆ [84, 53, … 6] │
    │ 48 ┆ 2022-10-30 00:01:36 ┆ 27 ┆ 10 ┆ 56.5 ┆ [53, 46, … 27] │
    │ 49 ┆ 2022-10-30 00:01:38 ┆ 33 ┆ 10 ┆ 54.5 ┆ [46, 56, … 33] │
    └─────┴─────────────────────┴──────┴───────────────┴───────────┴─────────────────┘


    Should I just now resort back to looping through the rand_val_list column and send each grouped values list to my function? Or is there a better polars way?

    Thanks again!

    Continue reading...

Compartilhe esta Página