Iliya R
08/21/2024, 6:29 PMThierry Jean
08/21/2024, 6:31 PM.execute() call? If you request nodes between Parallelizable and Collect, it will try to eagerly collect values into a list instead of "one at a time" in each branchIliya R
08/21/2024, 6:36 PMThierry Jean
08/21/2024, 6:40 PMinputs is a list of file paths
• from code to reduce_df you load multiple datasets using a custom DataLoader (let's say its many partition of .parquet )
• the Collect node merges the partitioned dataset
• you do downstream computation
If I understand, your DataLoader works, but once you added Parallelizable/Collect, it no longer worked?Stefan Krawczyk
08/21/2024, 6:41 PMStefan Krawczyk
08/21/2024, 6:46 PMimport pandas as pd
from hamilton.function_modifiers import load_from, source
from hamilton.htypes import Parallelizable, Collect
def code(inputs: list) -> Parallelizable[str]:
for _code in inputs:
yield _code
@load_from.csv(path=source("code"))
def data_set(df: pd.DataFrame) -> pd.DataFrame:
# some processing
return df
def reduced_df(data_set: Collect[pd.DataFrame]) -> pd.DataFrame:
return pd.concat(data_set)
def rest_of_my_computation(reduced_df: pd.DataFrame) -> object:
# do something
return reduced_df
this works for me:
from hamilton import driver
dr = driver.Builder().with_modules(example).enable_dynamic_execution(allow_experimental_mode=True).build()
dr.execute(["rest_of_my_computation"], inputs={"inputs": ["a", "b", "c"]})Iliya R
08/21/2024, 6:51 PM# data_loading.py
def symbol_generator(symbols: Iterable[str]) -> Parallelizable[str]:
yield from symbols
@dataclass(frozen=True)
class CustomLoader(DataLoader):
"""Loads data using the API."""
symbol: str
# other params
def load_data(self, _: type[type]) -> tuple[pd.DataFrame, dict[str, Any]]:
raw_df: pd.DataFrame = (
...
)
return raw_df, get_dataframe_metadata(raw_df)
@classmethod
def applicable_types(cls) -> Collection[type]:
return [pd.DataFrame]
@classmethod
def name(cls) -> str:
return "mdq"
# data_type_q.py
@load_from.mdq(
symbol=source("symbol"),
# other parameters
)
def data_type_q(input_data: pd.DataFrame) -> pd.DataFrame:
return input_data
@subdag(
data_loading, # this is where `symbol_generator` is defined
inputs={
"symbol": source("symbol_generator"),
# other params
},
)
def tagged_q(data_type_q: pd.DataFrame, symbol: str) -> pd.DataFrame:
return data_type_q.assign(symbol=symbol)
def mdq_all_symbols(tagged_q: Collect[pd.DataFrame]) -> pd.DataFrame:
mdq_df: pd.DataFrame = pd.concat(tagged_q, axis=0, sort=True, ignore_index=True)
return mdq_df.set_index(["timestamp", "symbol"]).sort_index()
So my custom loader class gets initialized with the full list of values, then in load_data I get multiple symbols.Iliya R
08/21/2024, 7:29 PMIliya R
08/21/2024, 7:30 PM@subdag is I need the list of symbols for multiple loading processes, and so I need to consume it more than onceElijah Ben Izzy
08/22/2024, 5:10 PMIliya R
08/22/2024, 5:21 PMElijah Ben Izzy
08/22/2024, 5:22 PMElijah Ben Izzy
08/22/2024, 5:22 PMElijah Ben Izzy
08/22/2024, 5:26 PMsymbol as a function. Due to limitations (that I think you mentioned on github) symbol_generator is not allowed to be consumed twice. So a dummy (as I think you suggested above/hinted at) is actually a nice way to do it
# data_loading.py
from dataclasses import dataclass
from typing import Iterable, Collection, Any
import pandas as pd
from hamilton.function_modifiers import source, load_from, subdag
from hamilton.htypes import Parallelizable, Collect
from hamilton.io.data_adapters import DataLoader
def symbol_generator(symbols: Iterable[str]) -> Parallelizable[str]:
yield from symbols
@dataclass(frozen=True)
class CustomLoader(DataLoader):
"""Loads data using the API."""
symbol: str
# other params
def load_data(self, _: type[type]) -> tuple[pd.DataFrame, dict[str, Any]]:
raw_df: pd.DataFrame = pd.DataFrame({"timestamp": [1, 2, 3], "value": [1, 2, 3]})
return raw_df, {}
@classmethod
def applicable_types(cls) -> Collection[type]:
return [pd.DataFrame]
@classmethod
def name(cls) -> str:
return "mdq"
from hamilton.registry import register_adapter
register_adapter(CustomLoader)
def symbol(symbol_generator: str) -> str:
return symbol_generator
@load_from.mdq(
symbol=source("symbol"),
# other parameters
)
def data_type_q(input_data: pd.DataFrame) -> pd.DataFrame:
return input_data
def tagged_q(data_type_q: pd.DataFrame, symbol: str) -> pd.DataFrame:
return data_type_q.assign(symbol=symbol)
def mdq_all_symbols(tagged_q: Collect[pd.DataFrame]) -> pd.DataFrame:
mdq_df: pd.DataFrame = pd.concat(tagged_q, axis=0, sort=True, ignore_index=True)
return mdq_df.set_index(["timestamp", "symbol"]).sort_index()
if __name__ == "__main__":
import __main__
from hamilton import driver
dr = driver.Builder().with_modules(__main__).enable_dynamic_execution(allow_experimental_mode=True).build()
dr.display_all_functions("./output.png")
dr.execute(["mdq_all_symbols"], inputs={"symbols": ["a", "b", "c"]})Elijah Ben Izzy
08/22/2024, 5:26 PMStefan Krawczyk
08/22/2024, 5:27 PMStefan Krawczyk
08/22/2024, 5:28 PMIliya R
08/22/2024, 5:57 PMIliya R
08/22/2024, 5:58 PMIliya R
08/22/2024, 5:59 PMElijah Ben Izzy
08/22/2024, 6:05 PMsymbol now accepts/passes through symbol_generator. All the others refer to symbol .This made it clearer for me but I realized it’s not a documented bug so it might not be necessary. I just like it as it means there’s one parallel edge in and one collect out.Iliya R
08/22/2024, 6:09 PMElijah Ben Izzy
08/22/2024, 6:59 PMIliya R
08/25/2024, 2:00 PMElijah Ben Izzy
08/26/2024, 5:57 AMIliya R
08/26/2024, 5:58 AMIliya R
08/26/2024, 6:04 AMElijah Ben Izzy
08/26/2024, 6:08 AMIliya R
08/26/2024, 6:24 AM