Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
96bc515
Added localCheckPointInterval
ConorWorthington Jun 29, 2026
b581ff1
Update pipeline.py
ConorWorthington Jun 30, 2026
32502cc
Update single_feature_array_standard_scale.py
ConorWorthington Jun 30, 2026
4e367d3
Update standard_scale.py
ConorWorthington Jun 30, 2026
2d2f75a
Update conditional_standard_scale.py
ConorWorthington Jun 30, 2026
f16c0ef
Update base.py
ConorWorthington Jun 30, 2026
8089c4f
Update bucketize.py
ConorWorthington Jun 30, 2026
9453119
Update bucketize.py
ConorWorthington Jul 1, 2026
06043b5
Update conditional_standard_scale.py
ConorWorthington Jul 1, 2026
b1096ce
Update single_feature_array_standard_scale.py
ConorWorthington Jul 1, 2026
70cd178
Update standard_scale.py
ConorWorthington Jul 1, 2026
c3db6b7
Update bucketize.py
ConorWorthington Jul 1, 2026
847f181
Update base.py
ConorWorthington Jul 1, 2026
6d5964a
Update pipeline.py
ConorWorthington Jul 1, 2026
ff3e9f7
Update pipeline.py
ConorWorthington Jul 1, 2026
f8cff4d
Update standard_scale.py
ConorWorthington Jul 2, 2026
5bb8cd9
Update single_feature_array_standard_scale.py
ConorWorthington Jul 2, 2026
4c470d4
Update conditional_standard_scale.py
ConorWorthington Jul 2, 2026
a8a8750
Update transform_utils.py
ConorWorthington Jul 2, 2026
71cf01a
Update pyproject.toml
ConorWorthington Jul 2, 2026
6c4e18f
Update pipeline.py
ConorWorthington Jul 6, 2026
771ab9c
Update pipeline.py
ConorWorthington Jul 6, 2026
d57d2a4
perf: persist during scaler fit and add pipeline fit-optimisation tests
Aug 5, 2026
a2985e1
Merge pull request #1 from ConorWorthington/pipeline-fit-optimisations
ConorWorthington Aug 5, 2026
94dea39
Patch black
ConorWorthington Aug 5, 2026
4f5cea2
Patch error
ConorWorthington Aug 5, 2026
3878d2f
Patch test case
ConorWorthington Aug 5, 2026
1918090
Remove complexity, remove comments
ConorWorthington Aug 5, 2026
96fc184
Patch for lint
ConorWorthington Aug 6, 2026
6af4ba4
Patch for linter
ConorWorthington Aug 6, 2026
78fef71
refactor: address PR review feedback on pipeline fit optimisations
Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ requires-python = ">=3.10,<3.13"
dependencies = [
"pyspark>=3.4.0,<4.0.0",
"pandas>=1.3.4,<3.0.0",
"pyarrow>=4.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Adding new dependencies needs a uv lock pls

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Lock updated

"networkx>=2.6.3,<3.0.0",
"pyfarmhash>=0.3.2,<0.4.0",
"keras>=3.0.0,<4.0.0",
Expand Down
48 changes: 32 additions & 16 deletions src/kamae/spark/estimators/conditional_standard_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pyspark.ml.param import Param, Params, TypeConverters
from pyspark.sql import Column, DataFrame
from pyspark.sql.types import ArrayType, DataType, DoubleType, FloatType
from pyspark.storagelevel import StorageLevel

from kamae.keras.core.backend import ALL_BACKENDS
from kamae.spark.params import (
Expand Down Expand Up @@ -378,22 +379,37 @@ def _fit(self, dataset: DataFrame) -> "ConditionalStandardScaleTransformer":
mask_val = self.getMaskValues()[i]
dataset = dataset.filter(mask_op(F.col(mask_col), mask_val))

# Collect a single row to driver and get the length.
# We assume all subsequent rows have the same length.
row = dataset.select(input_col).first()
if row is None:
raise ValueError("No data left after application of mask conditions.")
array_size = np.array((row[0])).shape[-1]

# Calculate the moments
if self.getScalingFunction().lower() == "standard":
return self._fit_standard(
dataset, input_col, input_column_dtype, array_size
)
elif self.getScalingFunction().lower() == "binary":
return self._fit_binary(dataset, input_col, input_column_dtype, array_size)
else:
raise ValueError(f"Unknown scaling function: {self.getScalingFunction()}.")
# Persist so the array-size probe and the moments aggregation reuse a
# materialised result instead of re-scanning the (masked) upstream lineage
# twice. Guarded so we do not double-persist data the caller already cached.
already_cached = dataset.storageLevel.useMemory or dataset.storageLevel.useDisk
if not already_cached:
dataset = dataset.persist(StorageLevel.MEMORY_AND_DISK)

try:
# Collect a single row to driver and get the length.
# We assume all subsequent rows have the same length.
row = dataset.select(input_col).first()
if row is None:
raise ValueError("No data left after application of mask conditions.")
array_size = np.array((row[0])).shape[-1]

# Calculate the moments
if self.getScalingFunction().lower() == "standard":
return self._fit_standard(
dataset, input_col, input_column_dtype, array_size
)
elif self.getScalingFunction().lower() == "binary":
return self._fit_binary(
dataset, input_col, input_column_dtype, array_size
)
else:
raise ValueError(
f"Unknown scaling function: {self.getScalingFunction()}."
)
finally:
if not already_cached:
dataset.unpersist()

def _fit_binary(
self,
Expand Down
62 changes: 38 additions & 24 deletions src/kamae/spark/estimators/single_feature_array_standard_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from pyspark import keyword_only
from pyspark.sql import DataFrame
from pyspark.sql.types import ArrayType, DataType, DoubleType, FloatType
from pyspark.storagelevel import StorageLevel

from kamae.keras.core.backend import ALL_BACKENDS
from kamae.spark.params import (
Expand Down Expand Up @@ -113,32 +114,45 @@ def _fit(self, dataset: DataFrame) -> "StandardScaleTransformer":
Got {input_column_type} instead."""
)

# Collect a single row to driver and get the length.
# We assume all subsequent rows have the same length.
array_size = np.array((dataset.select(self.getInputCol()).first()[0])).shape[-1]

# Flatten the array to a single array.
# Will do nothing if the array is not nested.
flattened_array_col = flatten_nested_arrays(
column=F.col(self.getInputCol()), column_data_type=input_column_type
)

mean_and_stddev_dict: Dict[str, float] = (
dataset.select(F.explode(flattened_array_col).alias(self.getInputCol()))
.withColumn(
"mask",
F.when(
F.col(self.getInputCol()) == F.lit(self.getMaskValue()), 1
).otherwise(0),
# Persist so the array-size probe and the moments aggregation reuse a
# materialised result instead of re-scanning the upstream lineage twice.
# Guarded so we do not double-persist data the caller already cached.
already_cached = dataset.storageLevel.useMemory or dataset.storageLevel.useDisk
if not already_cached:
dataset = dataset.persist(StorageLevel.MEMORY_AND_DISK)

try:
# Collect a single row to driver and get the length.
# We assume all subsequent rows have the same length.
array_size = np.array(
(dataset.select(self.getInputCol()).first()[0])
).shape[-1]

# Flatten the array to a single array.
# Will do nothing if the array is not nested.
flattened_array_col = flatten_nested_arrays(
column=F.col(self.getInputCol()), column_data_type=input_column_type
)
.filter(F.col("mask") == F.lit(0))
.agg(
F.mean(self.getInputCol()).alias("mean"),
F.stddev_pop(self.getInputCol()).alias("stddev"),

mean_and_stddev_dict: Dict[str, float] = (
dataset.select(F.explode(flattened_array_col).alias(self.getInputCol()))
.withColumn(
"mask",
F.when(
F.col(self.getInputCol()) == F.lit(self.getMaskValue()), 1
).otherwise(0),
)
.filter(F.col("mask") == F.lit(0))
.agg(
F.mean(self.getInputCol()).alias("mean"),
F.stddev_pop(self.getInputCol()).alias("stddev"),
)
.first()
.asDict()
)
.first()
.asDict()
)
finally:
if not already_cached:
dataset.unpersist()
mean: List[float] = [mean_and_stddev_dict["mean"] for _ in range(array_size)]
stddev: List[float] = [
mean_and_stddev_dict["stddev"] for _ in range(array_size)
Expand Down
84 changes: 49 additions & 35 deletions src/kamae/spark/estimators/standard_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pyspark import keyword_only
from pyspark.sql import DataFrame
from pyspark.sql.types import ArrayType, DataType, DoubleType, FloatType
from pyspark.storagelevel import StorageLevel

from kamae.keras.core.backend import ALL_BACKENDS
from kamae.spark.params import (
Expand Down Expand Up @@ -113,41 +114,54 @@ def _fit(self, dataset: DataFrame) -> "StandardScaleTransformer":
else:
input_col = F.col(self.getInputCol())

# Collect a single row to driver and get the length.
# We assume all subsequent rows have the same length.
array_size = np.array((dataset.select(input_col).first()[0])).shape[-1]

element_struct = construct_nested_elements_for_scaling(
column=input_col,
column_datatype=input_column_type,
array_dim=array_size,
)

mean_cols = [
F.mean(
F.when(
F.col(f"element_struct.element_{i}") == F.lit(self.getMaskValue()),
F.lit(None),
).otherwise(F.col(f"element_struct.element_{i}"))
).alias(f"mean_{i}")
for i in range(1, array_size + 1)
]

stddev_cols = [
F.stddev_pop(
F.when(
F.col(f"element_struct.element_{i}") == F.lit(self.getMaskValue()),
F.lit(None),
).otherwise(F.col(f"element_struct.element_{i}"))
).alias(f"stddev_{i}")
for i in range(1, array_size + 1)
]

metric_cols = mean_cols + stddev_cols

mean_and_stddev_dict = (
dataset.select(element_struct).agg(*metric_cols).first().asDict()
)
# Persist so the array-size probe and the moments aggregation reuse a
# materialised result instead of re-scanning the upstream lineage twice.
# Guarded so we do not double-persist data the caller already cached.
already_cached = dataset.storageLevel.useMemory or dataset.storageLevel.useDisk
if not already_cached:
dataset = dataset.persist(StorageLevel.MEMORY_AND_DISK)

try:
# Collect a single row to driver and get the length.
# We assume all subsequent rows have the same length.
array_size = np.array((dataset.select(input_col).first()[0])).shape[-1]

element_struct = construct_nested_elements_for_scaling(
column=input_col,
column_datatype=input_column_type,
array_dim=array_size,
)

mean_cols = [
F.mean(
F.when(
F.col(f"element_struct.element_{i}")
== F.lit(self.getMaskValue()),
F.lit(None),
).otherwise(F.col(f"element_struct.element_{i}"))
).alias(f"mean_{i}")
for i in range(1, array_size + 1)
]

stddev_cols = [
F.stddev_pop(
F.when(
F.col(f"element_struct.element_{i}")
== F.lit(self.getMaskValue()),
F.lit(None),
).otherwise(F.col(f"element_struct.element_{i}"))
).alias(f"stddev_{i}")
for i in range(1, array_size + 1)
]

metric_cols = mean_cols + stddev_cols

mean_and_stddev_dict = (
dataset.select(element_struct).agg(*metric_cols).first().asDict()
)
finally:
if not already_cached:
dataset.unpersist()
mean = [mean_and_stddev_dict[f"mean_{i}"] for i in range(1, array_size + 1)]
stddev = [mean_and_stddev_dict[f"stddev_{i}"] for i in range(1, array_size + 1)]

Expand Down
Loading
Loading