blob: d87acc92f7cbb9a0ff1e02ee9bde4dcafddcf09b [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import typing
import dask.dataframe as dd
import numpy as np
import pandas as pd
import pytest
from dask.delayed import delayed
from distributed import Client
from hamilton import driver
from hamilton.plugins import h_dask
from .resources import example_module, smoke_screen_module
@pytest.fixture(scope="session")
def client():
with Client(set_as_default=False).as_current() as client:
yield client
def test_dask_graph_adapter_simple(client):
initial_columns = {
# NOTE: here you could load individual columns from a parquet file
# in delayed manner, e.g., using sth. along the lines of
# delayed(lambda: pd.read_parquet(path, columns=[col])[col])()
"signups": delayed(lambda: pd.Series([1, 10, 50, 100, 200, 400]))(),
"spend": delayed(lambda: pd.Series([10, 10, 20, 40, 40, 50]))(),
}
dr = driver.Driver(initial_columns, example_module, adapter=h_dask.DaskGraphAdapter(client))
output_columns = [
"spend",
"signups",
"avg_3wk_spend",
"spend_per_signup",
]
df = dr.execute(output_columns)
assert set(df) == set(output_columns)
expected_column = pd.Series(
[0.0, 0.0, 13.33333, 23.33333, 33.33333, 43.33333], name="avg_3wk_spend"
)
pd.testing.assert_series_equal(
df.avg_3wk_spend.fillna(0.0), expected_column
) # fill na to get around NaN
# TODO: do some more asserting?
def test_smoke_screen_module(client):
config = {"region": "US"}
dr = driver.Driver(config, smoke_screen_module, adapter=h_dask.DaskGraphAdapter(client))
output_columns = [
"raw_acquisition_cost",
"pessimistic_net_acquisition_cost",
"neutral_net_acquisition_cost",
"optimistic_net_acquisition_cost",
"series_with_start_date_end_date",
]
df = dr.execute(
inputs={"date_range": {"start_date": "20200101", "end_date": "20220801"}},
final_vars=output_columns,
)
epsilon = 0.00001
assert abs(df["raw_acquisition_cost"].mean() - 0.393808) < epsilon
assert abs(df["pessimistic_net_acquisition_cost"].mean() - 0.420769) < epsilon
assert abs(df["neutral_net_acquisition_cost"].mean() - 0.405582) < epsilon
assert abs(df["optimistic_net_acquisition_cost"].mean() - 0.399363) < epsilon
assert df["series_with_start_date_end_date"].iloc[0] == "date_20200101_date_20220801"
# The following tests are for exercising the DaskDataFrameResult.build_result function
dd_test_cases = [
# single_scalar
({"a": 1}, pd.DataFrame({"a": [1]})),
# single_series
({"a": pd.Series([1, 2, 3])}, pd.DataFrame({"a": [1, 2, 3]})),
# single_dataframe
({"a": pd.DataFrame({"a": [1, 2, 3]})}, pd.DataFrame({"a.a": [1, 2, 3]})),
# multiple_series
(
{"a": pd.Series([1, 2, 3]), "b": pd.Series([4, 5, 6])},
pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}),
),
# multiple_dataframe
(
{"a": pd.DataFrame({"a": [1, 2, 3]}), "b": pd.DataFrame({"b": [4, 5, 6]})},
pd.DataFrame({"a.a": [1, 2, 3], "b.b": [4, 5, 6]}),
),
# series_and_dataframe
(
{"a": pd.Series([1, 2, 3]), "b": pd.DataFrame({"b": [4, 5, 6]})},
pd.DataFrame({"a": [1, 2, 3], "b.b": [4, 5, 6]}),
),
# series_and_scalar
(
{"b": pd.Series([4, 5, 6]), "a": 1, "c": "string"},
pd.DataFrame({"b": [4, 5, 6], "a": [1, 1, 1], "c": ["string", "string", "string"]}),
),
# dataframe_and_scalar
(
{"a": pd.DataFrame({"a": [1, 2, 3]}), "b": 1},
pd.DataFrame({"a.a": [1, 2, 3], "b": [1, 1, 1]}),
),
# dataframe_and_series
(
{
"a": pd.Series([1, 2, 3]),
"b": pd.DataFrame({"b": [1, 2, 3], "c": [1, 1, 1]}),
},
pd.DataFrame({"a": [1, 2, 3], "b.b": [1, 2, 3], "b.c": [1, 1, 1]}),
),
# multiple_series_and_scalar
(
{"a": pd.Series([1, 2, 3]), "b": pd.Series([4, 5, 6]), "c": 12},
pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [12, 12, 12]}),
),
# multiple_dataframes_and_scalar
(
{
"a": pd.DataFrame({"a": [1, 2, 3]}),
"b": pd.DataFrame({"b": [4, 5, 6], "d": [2, 2, 2]}),
"c": 12,
},
pd.DataFrame({"a.a": [1, 2, 3], "b.b": [4, 5, 6], "b.d": [2, 2, 2], "c": [12, 12, 12]}),
),
# multiple_series_and_dataframe
(
{
"d": pd.Series([11, 22, 33]),
"e": pd.Series([44, 55, 66]),
"c": pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}),
},
pd.DataFrame(
{
"d": [11, 22, 33],
"e": [44, 55, 66],
"c.a": [1, 2, 3],
"c.b": [4, 5, 6],
}
),
),
# multiple_series_and_dataframe_and_scalar
(
{
"d": pd.Series([11, 22, 33]),
"e": pd.Series([44, 55, 66]),
"c": pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}),
"f": 12,
},
pd.DataFrame(
{
"d": [11, 22, 33],
"e": [44, 55, 66],
"c.a": [1, 2, 3],
"c.b": [4, 5, 6],
"f": [12, 12, 12],
}
),
),
# numpy_arrays
(
{"a": np.array([1, 2, 3]), "b": np.array([4, 5, 6])},
pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}),
),
# lists
({"a": [1, 2, 3], "b": [4, 5, 6]}, pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})),
# tuples
({"a": (1, 2, 3), "b": (4, 5, 6)}, pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})),
]
dd_test_case_ids = [
"single_scalar",
"single_series",
"single_dataframe",
"multiple_series",
"multiple_dataframes",
"series_and_dataframe",
"series_and_scalar",
"dataframe_and_scalar",
"dataframe_and_series",
"multiple_series_and_scalar",
"multiple_dataframes_and_scalar",
"multiple_series_and_dataframe",
"multiple_series_and_dataframe_and_scalar",
"numpy_arrays",
"lists",
"tuples",
]
@pytest.mark.parametrize(("outputs", "expected"), dd_test_cases, ids=dd_test_case_ids)
def test_DDFR_build_result_pandas(client, outputs: dict[str, typing.Any], expected: dd.DataFrame):
"""Tests using pandas objects works"""
actual = h_dask.DaskDataFrameResult.build_result(**outputs)
actual_pdf = actual.compute().convert_dtypes(dtype_backend="pyarrow")
expected_pdf = expected.convert_dtypes(dtype_backend="pyarrow")
pd.testing.assert_frame_equal(actual_pdf, expected_pdf)
@pytest.mark.parametrize(("outputs", "expected"), dd_test_cases, ids=dd_test_case_ids)
def test_DDFR_build_result_dask(client, outputs: dict[str, typing.Any], expected: dd.DataFrame):
"""Tests that using dask objects works."""
dask_outputs = {}
for k, v in outputs.items():
if isinstance(v, (pd.DataFrame, pd.Series)):
dask_outputs[k] = dd.from_pandas(v, npartitions=2)
else:
dask_outputs[k] = v
actual = h_dask.DaskDataFrameResult.build_result(**dask_outputs)
actual_pdf = actual.compute().convert_dtypes(dtype_backend="pyarrow")
expected_pdf = expected.convert_dtypes(dtype_backend="pyarrow")
pd.testing.assert_frame_equal(actual_pdf, expected_pdf)
def test_DDFR_build_result_mixed(client):
"""Tests that using dask & pandas objects works."""
outputs = {
"d": dd.from_pandas(pd.Series([11, 22, 33]), npartitions=2),
"e": pd.Series([44, 55, 66]),
"c": pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}),
"g": dd.from_pandas(pd.DataFrame({"a2": [1, 2, 3], "b2": [4, 5, 6]}), npartitions=2),
"f": 12,
}
expected_df = pd.DataFrame(
{
"d": [11, 22, 33],
"e": [44, 55, 66],
"c.a": [1, 2, 3],
"c.b": [4, 5, 6],
"g.a2": [1, 2, 3],
"g.b2": [4, 5, 6],
"f": [12, 12, 12],
}
)
actual = h_dask.DaskDataFrameResult.build_result(**outputs)
actual_pdf = actual.compute().convert_dtypes(dtype_backend="pyarrow")
expected_pdf = expected_df.convert_dtypes(dtype_backend="pyarrow")
pd.testing.assert_frame_equal(actual_pdf, expected_pdf)
def test_DDFR_build_result_scalar_first_edge_case(client):
"""Tests the scalar edge case where we don't know how long to make the series from it."""
actual = h_dask.DaskDataFrameResult.build_result(
**{"a": 1, "b": dd.from_pandas(pd.Series([4, 5, 6]), npartitions=1)}
)
expected = pd.DataFrame({"a": [1, pd.NA, pd.NA], "b": [4, 5, 6]})
actual_pdf = actual.compute().convert_dtypes(dtype_backend="pyarrow")
expected_pdf = expected.convert_dtypes(dtype_backend="pyarrow")
pd.testing.assert_frame_equal(actual_pdf, expected_pdf)
def test_DDFR_build_result_varied_index_lengths(client):
"""Tests what happens when the index lengths are different.
So this test enshrines the current logic of how we handle scalars and series/dfs with different
lengths. We could do smarter things here (e.g. make scalars=max(len(...))), but this is the current behavior.
Our thinking is that you should control this via the order of the outputs requested for now.
i.e. re-arranging the outputs dictionary should give you the desired result that you want.
"""
outputs = {
"a": 1,
"b": dd.from_pandas(pd.Series([4, 5, 6]), npartitions=1),
"c": dd.from_pandas(pd.Series([10, 6]), npartitions=1),
"d": pd.DataFrame({"a": [11, 22, 33, 44], "b": [44, 55, 66, 77]}),
"e": "a",
}
actual = h_dask.DaskDataFrameResult.build_result(**outputs)
expected = pd.DataFrame(
{
"a": [1, pd.NA, pd.NA, pd.NA],
"b": [4, 5, 6, pd.NA],
"c": [10, 6, pd.NA, pd.NA],
"d.a": [11, 22, 33, 44],
"d.b": [44, 55, 66, 77],
"e": ["a", "a", "a", pd.NA],
}
)
actual_pdf = actual.compute().convert_dtypes(dtype_backend="pyarrow")
expected_pdf = expected.convert_dtypes(dtype_backend="pyarrow")
pd.testing.assert_frame_equal(actual_pdf, expected_pdf)
def test_DDFR_build_result_dd_scalar(client):
"""Tests that dask scalars work as expected."""
outputs = {
"a": dd.from_pandas(pd.Series([4, 5, 6]), npartitions=1),
"b": dd.from_pandas(pd.Series([10, 6, 3]), npartitions=1),
}
outputs["c"] = outputs["a"].sum()
actual = h_dask.DaskDataFrameResult.build_result(**outputs)
expected = pd.DataFrame(
{
"a": [4, 5, 6],
"b": [10, 6, 3],
"c": [15, 15, 15],
}
)
actual_pdf = actual.compute().convert_dtypes(dtype_backend="pyarrow")
expected_pdf = expected.convert_dtypes(dtype_backend="pyarrow")
pd.testing.assert_frame_equal(actual_pdf, expected_pdf)