Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(python): add parametric tests for groupby_dynamic #9334

Closed

Conversation

MarcoGorelli
Copy link
Collaborator

@MarcoGorelli MarcoGorelli commented Jun 12, 2023

@github-actions github-actions bot added python Related to Python Polars test Related to the test suite labels Jun 12, 2023
Copy link

@honno honno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind I'm pretty ignorant on the problem domain here, but it looks good to me!

On purposely restricting strategies so they don't trigger known bugs—I think it makes sense here, given the sheer number of permutations. Ideally one would have the strategies cover all supposedly-valid input and slapping a xfail when needed, but that gets annoying fast when you have such a complex domain with multiple known bugs.

I did a diff on the two tests and it does look annoying enough to refactor, not that refactoring does much good when things have only been repeated once. Just to say if there was a way to have these tests neatly refactored (i.e. a big _test_against_pandas function called in both tests, or a parametrized arg like test_weekly: bool in a single test), defo play around with it if you haven't already.

data: st.DataObject,
) -> None:
pl_every, pd_alias = every_alias
assume(timezone in zoneinfo.available_timezones())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you instead construct the strategy for timezone to just be the available timezones? e.g. timezone=st.sampled_from(zoneinfo.available_timezones())

@alexander-beedie
Copy link
Collaborator

alexander-beedie commented Jun 12, 2023

At the moment these tests are very bespoke, in that they don't use any of the existing polars hypothesis primitives/strategies; It would be nice if we could pull out the datetime (with timezone) generating strategy as a customisable create_datetime_strategy helper in the same vein as the existing create_list_strategy (timezones have been on my radar for a while, but I hadn't got round to it yet - currently datetime generation just varies by time_unit).

Either I can have a crack at that, or I can leave it to one of you and then review? Either option is fine by me... ;)

@MarcoGorelli
Copy link
Collaborator Author

Thanks @honno ! Indeed, it was possible to simplify a bit, thanks for suggesting that

And thanks @alexander-beedie for taking a look! Which part are you suggesting to make a strategy for - creating a polars Series with Datetime dtype?

@MarcoGorelli MarcoGorelli marked this pull request as ready for review June 12, 2023 09:04
@alexander-beedie
Copy link
Collaborator

alexander-beedie commented Jun 12, 2023

And thanks @alexander-beedie for taking a look! Which part are you suggesting to make a strategy for - creating a polars Series with Datetime dtype?

We can already do that easily enough, just not with customisable timezones - would be nice to improve that so we don't have to go bespoke once a timezone comes into play :)

>
>
Co-authored-by: honno <[email protected]>
@MarcoGorelli MarcoGorelli marked this pull request as draft June 12, 2023 09:26
@honno
Copy link

honno commented Jun 12, 2023

Okay to reconstruct the current way of generating the original df example, it goes something like

@st.composite
def timey_wimey_dataframes(draw: st.DrawFn) -> pl.DataFrame:
    datetimes = draw(
        st.lists(
            st.datetimes(
                min_value=dt.datetime(1980, 1, 1),
                # Can't currently go beyond 2038, see
                # https://github.com/pola-rs/polars/issues/9315
                max_value=dt.datetime(2038, 1, 1),
            ),
            min_size=1,
        )
    )
    timezone = draw(st.sampled_from(list(zoneinfo.available_timezones())))
    
    nrows = len(datetimes)
    values = draw(
        st.lists(st.floats(10, 20), min_size=nrows, max_size=nrows), label="values"
    )

    try:
        df = (
            pl.DataFrame({"ts": datetimes, "values": values})
            .sort("ts")
            .with_columns(
                pl.col("ts").dt.replace_time_zone("UTC").dt.convert_time_zone(timezone)
            )
        )
    except pl.exceptions.ComputeError as exp:
        assert "unable to parse time zone" in str(exp)  # sanity check
        reject()

    return df

strat = timey_wimey_dataframes()

Using polars.testing I suppose you do something like

strat = pl.testing.dataframes(
    cols=[
        pl.testing.column(
            name="ts",
            dtype=pl.Datetime(),
            strategy=st.datetimes(
                min_value=dt.datetime(1980, 1, 1),
                max_value=dt.datetime(2038, 1, 1),
                timezones=st.sampled_from(list(zoneinfo.available_timezones())),
            ),
        ),
        pl.testing.column(name="values", dtype=pl.Float64(), strategy=st.floats(-10, 10)),
    ]
).map(lambda df: df.sort("ts"))

? Hopefully this is a good start @MarcoGorelli

In any case, I used the timezones strategy directly in st.datetimes(), but I wonder if that causes problems?

(I haven't actually tried this—I suppose polars.testing doesn't show up from like PyPI builds, so will have to try this later when I set up polars for development)

@MarcoGorelli MarcoGorelli marked this pull request as ready for review June 14, 2023 13:20
@MarcoGorelli
Copy link
Collaborator Author

cool, I've given this a go - thanks @honno ! any further suggestions?

number: int,
) -> None:
nrows = len(time_series)
values = pl.Series(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if testing.parametric.series can be utilised here?

Haven't tried this myself yet, but something like

Suggested change
values = pl.Series(
values = data.draw(pl.testing.parametric.series(strategy=st.floats(10, 20)), label="values")

(Incase you missed it, you can annotate draws with labels, which can make debugging easier for more complicated tests. No biggie tho.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@honno: Interesting; do you think it would be worth putting any labels into the underlying primitives themselves?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@honno: Interesting; do you think it would be worth putting any labels into the underlying primitives themselves?

I think I'm correct in saying that there's no real point putting labels inside of @st.composite-functions, as Hypothesis would only report the overall thing been drawn at the test level, i.e. test_foo(data): data.draw(dataframes(...), label="only the final df is reported"). Labeling seems more useful anyway when a bespoke thing is for a test method.

Also, it's really cool that these primitives exist!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the tests I think a few more native params might be useful too; some generic min_value & max_value args, for example... Don't hesitate to request anything you think would be a time-saver / useful :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I see what you mean exactly, could you clarify what you're suggesting I change please?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! That’s more of a note to myself than you, sorry… Just thinking I can extend the primitives a bit to further simplify common cases 🤣

@MarcoGorelli MarcoGorelli requested a review from honno June 26, 2023 20:01
Copy link

@honno honno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the usage of Hypothesis, everything LGTM!

Already noted to Marco before that I would name strategy-factories (e.g. @st.composite-decorated functions that return st.SearchStrategy objects) be named "thing-it-generates" (preferably in plural), like dataframes() or series(). But I see the existing convention in polars.testing.parametric.strategies is to name these with the strategy_ prefix, so no biggie.

@MarcoGorelli
Copy link
Collaborator Author

MarcoGorelli commented Jun 27, 2023

thanks!

@alexander-beedie any objections to getting this in? it would add 20 seconds or so to CI, but alongside the unit tests (which this is absolutely not a replacement for) would increase confidence in future refactors of groupby_dynamic a fair bit

EDIT: marking as draft for now, gonna see if I can reduce the runtime whilst keeping the extra safety it provides

@MarcoGorelli MarcoGorelli marked this pull request as draft June 28, 2023 07:29
@MarcoGorelli
Copy link
Collaborator Author

closing for now to clear the queue, will get back to this when I get a chance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
python Related to Python Polars test Related to the test suite
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants