feat: array contains - #67
Conversation
|
Could I please be also given write access so I wouldn't need to create the fork? 🙏 |
georyetti
left a comment
There was a problem hiding this comment.
Some comments:
- Keras layer can contain more params to make it more flexible. We can set these as fixed from Spark side
- I think this can return booleans and the user can set output dtypes for auto casting to floats
- Can we use the Spark array helper functions to support nested arrays
| raise ValueError(f"Expected 2 inputs, got {len(inputs)} inputs instead.") | ||
|
|
||
| array, value = inputs | ||
| any_match = ops.any(ops.equal(array, value), axis=-1, keepdims=True) |
There was a problem hiding this comment.
Can we make axis a param of this layer? Then its more useful for a user that just reuses keras layers directly? We can set axis=-1 from Spark side
There was a problem hiding this comment.
Same for keepdims too pls
| output_col = ( | ||
| F.when(F.array_contains(F.col(arr_c), F.col(val_c)), F.lit(1.0)) | ||
| .otherwise(F.lit(0.0)) | ||
| .cast(DoubleType()) | ||
| ) |
There was a problem hiding this comment.
Can we reuse the helper methods we have for array computation here? Then it would support nested arrays also in Spark.
| "int8", | ||
| "uint8", | ||
| "int16", | ||
| "uint16", | ||
| "int32", | ||
| "uint32", | ||
| "int64", | ||
| "uint64", |
There was a problem hiding this comment.
Does this work for int values? Or just int arrays? If so can we add a test please. All tests currently are just with float values
There was a problem hiding this comment.
Yeah should work for both arrays and scalars, added a test
|
|
||
| array, value = inputs | ||
| any_match = ops.any(ops.equal(array, value), axis=-1, keepdims=True) | ||
| return ops.cast(any_match, "float32") |
There was a problem hiding this comment.
Is it expected that array contains returns a float32? I would expect a boolean. The user can set output_dtype="float32" to get this behaviour so I would say we should preserve booleans here
5583b8b to
8133b18
Compare
georyetti
left a comment
There was a problem hiding this comment.
Two comments:
- Pedantic one about using helpers, you have used some more internal ones, we have an easier wrapper for this.
- Do we want to allow a pattern where the scalar value is just a normal constant. Like we do with the Divide/Multiply transformers?
| nesting_level, elem_t = get_array_nesting_level_and_element_dtype(arr_t) | ||
| if not isinstance(elem_t, _NUMERIC_TYPES): | ||
| raise TypeError(f"arrayCol '{arr_c}' element must be numeric, got {elem_t}") | ||
|
|
||
| if not isinstance(val_t, _NUMERIC_TYPES): | ||
| raise TypeError(f"valueCol '{val_c}' must be numeric, got {val_t}") | ||
|
|
||
| # Apply array_contains at the innermost level | ||
| contains_func = nested_transform( | ||
| func=lambda x: F.array_contains(x, F.col(val_c)), | ||
| nest_level=nesting_level - 1, | ||
| ) | ||
| output_col = contains_func(F.col(arr_c)) |
There was a problem hiding this comment.
We have a helper for this that makes it a bit simpler, can we use this? single_input_single_output_array_transform
Also there is no need to check for numeric types as this is done by the BaseTransformer using compatible_dtypes
| inputCols: Optional[List[str]] = None, | ||
| outputCol: Optional[str] = None, | ||
| inputDtype: Optional[str] = None, | ||
| outputDtype: Optional[str] = None, | ||
| layerName: Optional[str] = None, | ||
| ) -> None: | ||
| """ | ||
| Initializes an ArrayContainsTransformer transformer. | ||
|
|
||
| :param inputCols: Input column names, given as `[arrayCol, valueCol]`. |
There was a problem hiding this comment.
Do we want to allow a single input col and a constant? So we can check if array_contains(x, -1) for example?
There was a problem hiding this comment.
Fair idea, generalised it
| output_col = single_input_single_output_array_transform( | ||
| input_col=array_col, | ||
| input_col_datatype=self.get_column_datatype(df, df.columns[0]), | ||
| func=lambda x: F.array_contains(x, value_col), |
There was a problem hiding this comment.
Just wanted to doulbe-check, array_contains can return NULL too, not just True/False, while for the Keras side ops.any(ops.equal(...)) always gives a boolean. if the array contains even a single NULL, array contains will result NULL overall. Is this the desired behaviour, to propagate the NULL? If not we can maybe wrap it with an explcit check?
There was a problem hiding this comment.
Good catch, wrapped in F.coalesce(..., F.lit(False))
| else: | ||
| raise ValueError(f"Expected 2 inputs, got {len(inputs)} inputs instead") | ||
|
|
||
| return ops.any(ops.equal(array, value), axis=self.axis, keepdims=self.keepdims) |
There was a problem hiding this comment.
Is the equality check here identical to what array_contains does on the Spark side? Just trying to understand whether there's any chance of a mismatch on exact equality.
I see above (line 111) the cast only runs for the constant branch, so I wasn't sure what happens when the value comes in as a separate column with a different dtype.
There was a problem hiding this comment.
Both use exact equality and inputDtype casts the array and value to the same dtype
There was a problem hiding this comment.
Thanks Mantas!
The inputDtype point holds when it's defined, but it defaults to None, and then nothing aligns the array and the constant, so each side reconciles float vs double its own way:
Spark widens the array to double, and Keras on the opposite direction narrows the constant to float32. It might be pedantic, since this only matters for constants that aren't exactly representable in float32, like 0.1: array_contains([0.1f], 0.1) → false in Spark (0.10000000149 ≠ 0.1), but true in Keras. (I checked the Spark side locally with claude.)
Could we cast the literal to the array's element type on the Spark side (or mirror Spark's widening in Keras), and add a parity test with inputDtype=None and a constant like 0.1 (that float32 is not enough to describe it) on a float array? The current parity tests only use integer arrays and integer constants (5.0 and 0.0), so the float path isn't covered.
There was a problem hiding this comment.
Nice, thank you! Made it so that behaviour is identical to #L111
Description
Adds an
ArrayContainstransformer and paired Keras layer that check whether a scalar value is contained in an array feature, outputting1.0if present and0.0otherwise.Keras Layer Checklist
_callmethod has been implemented in the new layer.compatible_dtypesproperty is defined in the new layer.@tf.keras.utils.register_keras_serializable(package=kamae.__name__).name,input_dtype, andoutput_dtypeas arguments to the constructor and that this is passed to the super constructor.get_configmethod.layersdirectory.Spark Transformer/Estimator Checklist
__init__andsetParamsmethods.Paramsclass here.compatible_dtypesproperty has been implemented to specify the input/output data types that my transformer/estimator supports.get_tf_layermethod.transformers/estimatorsdirectory.Readme Checklist