Dumb Q1: In this toy example, `paginated_data` ret...
# hamilton-help
a
Dumb Q1: In this toy example,
paginated_data
returns
List[Dict[str, Any]]
such as could be dropped into a
pd.DataFrame
. The result
data
is
{"paginated_data": [{… }
. Can I return a df without having to do something like
pd.DataFrame(data['paginated_data'])
? It matters because
final_vars
may be determined at runtime based on something like tags. Sure I've done this before but I forget…
Copy code
dr = (
    await async_driver.Builder()
    .with_config(config)
    .with_modules(transforms)
    .build()
)
data = await dr.execute(
    ["paginated_data"], inputs={"endpoint": endpoint, "request_params": request_params}
)
… and Q2: Suppose
paginated_data
is something like the following and we want to reuse the transform across various dataflows assembled dynamically using module composition, would it be better to try and write this as a materializer?
Copy code
async def paginated_data(
    endpoint: str,
    request_params: Dict[str, Any],
    *,
    api_settings: settings.Settings,
    pagination_request: schemas.PaginationRequest,
    pagination_params: Optional[schemas.PaginationParams] = None,
) -> List[Dict[str, Any]]:
    pagination_params = pagination_params or schemas.PaginationParams()
    async with pagination.PaginationService(api_settings) as pagination_service:
        result = await pagination_service.fetch_all_pages(
            endpoint,
            pagination_request=pagination_request,
            pagination_params=pagination_params,
            extra_params=request_params,
        )

    return result
… and Q3: Suppose that in some cases we want to do a groupby operation on the dataframe and apply a lightly configurable set of transforms to each group, what's the best pattern for that to preserve visibility of the transforms applied to each group?
👀 1
e
Hey! Some responses: 1. Yes, this is quite possible — if you use the right result builder (
result_builder = base.PandasDataFrameResult()
and pass it into
with_adapters(result_builder)
it’ll give you what you want! 2. You have a few ways of doing this a. A materializer makes a lot of sense here — it’s a common way of “loading” the data, so wrapping it in a class and tacking it on is good b. You can also make it in a module — E.G. a module called
data_loaders
that defines this, and then make subsequence modules declare
paginated_data
as parameters to their functions. IMO this is a really clean way to reuse that promotes code reuse + clearly organized naming. 3. I think you’re going to really appreciate the
@pipe_output
decorator (thanks @Jernj Frank for implementing!!). Given that it’s “lightly configurable”, this has a capability of having a per-step
when
condition, allowing you to pass in config data to the driver and use that to control the shape of the chain. Docs/code examples here: https://hamilton.dagworks.io/en/latest/reference/decorators/pipe/#pipe-output
Oh! Just realized we don’t support
@pipe_output
for async yet, but that’s not a crazy change to make.
Will open an issue to track
That said, I think it should work with a non-async function in an async context, so if you have a function that takes in
paginated_data
and passes through, you should be able to get it to work
I’ll test it out though to make sure
OK, yes, it works! So this is the workaround for now — you just need an intermediate node (unless the transformations are async, in which case it’s not supported yet).
Copy code
import asyncio

import pandas as pd

from hamilton import async_driver
from hamilton.function_modifiers import pipe_output, step, hamilton_exclude


async def data_input() -> pd.DataFrame:
    await asyncio.sleep(0.0001)
    return pd.DataFrame({
        "a": [1, 2, 3],
        "b": [4, 5, 6]
    })


def _groupby_a(d: pd.DataFrame) -> pd.DataFrame:
    return d.groupby("a").sum().reset_index()


def _groupby_b(d: pd.DataFrame) -> pd.DataFrame:
    return d.groupby("b").sum().reset_index()


@pipe_output(
    step(_groupby_a).when(groupby="a"),
    step(_groupby_b).when_not(groupby="a"),
)
def data(data_input: pd.DataFrame) -> pd.DataFrame:
    return data_input


@hamilton_exclude
async def main():
    import __main__
    dr = (await async_driver.Builder().with_modules(__main__).with_config(dict(groupby="b")).build())
    results = await dr.execute(["data"])
    print(results)


if __name__ == "__main__":
    asyncio.run(main())
a
Thanks @Elijah Ben Izzy, will give this a go today.
e
Yep! Let us know how much/if the issue above is a blocker for you, I think the workaround should help
a
Will do. QQ: When I do,
Copy code
result_builder = PandasDataFrameResult()
dr = (
    await async_driver.Builder()
    .with_config(config)
    .with_modules(transforms)
    .with_adapters(result_builder)
    .build()
)
data = await dr.execute(
    ["paginated_data"], inputs={"endpoint": endpoint, "request_params": request_params}
)
I get
Copy code
paginated_data
0	{'record_id': '0x57AE4108A24DD9E51468C03FA003A...
1	{'record_id': '0x3CDCFAD8388BD79054F69C71230CB...
2	{'record_id': '0x478481F7DBBE326F122B299DAE765...
which is what was confusing me before. It's the value of the
paginated_data
KV pair that needs to be dropped into the DF constructor. Maybe I can setup the transform differently? I think the issue is that the result builder envisages combining series but in this context I have data as records… if that makes sense?
e
Yeah, so I think this is probably the case for (a) turning it into a series to conform with what it expects or (b) creating a custom result builder
a
Okay. In the event I need to do the second option, is there an easy way of extracting the name of the final vars dynamically? In the past, when using module composition, I've done things like
@tag(data_product="final")
and some filtering to find the list to pass to
.execute(…
. But in this context I will want to select from various tag options at runtime and might need access the correct keys in a custom result builder.
e
So the result builder has
build_result
, which takes in
**outputs
— that’s a dict of the node name -> node returned value from the DAG
If you want more (E.G. tags), you’ll have to be smart: 1. Also have it extend
GraphConstructionHook
2. Loop through the nodes of the
graph
object, storing the tags 3. In the result builder function (
build_result
), look up the tags versus the names (use the 4. Decide how to combine accordingly But if you just need the names, then you can use the dictionary
a
Okay thanks. Lm think about whether I unequivocally need to solve that problem before trying to do it.
e
👍