[SYSTEMDS-3863] Add PowerTransformer built-in functions - #2499
[SYSTEMDS-3863] Add PowerTransformer built-in functions#2499WenliangCao wants to merge 16 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2499 +/- ##
============================================
+ Coverage 71.61% 71.65% +0.04%
- Complexity 50352 50490 +138
============================================
Files 1626 1628 +2
Lines 194664 195069 +405
Branches 38007 38042 +35
============================================
+ Hits 139408 139782 +374
- Misses 44328 44364 +36
+ Partials 10928 10923 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Add two DML builtins for the midterm PowerTransformer prototype: - powerTransform.dml estimates one Yeo-Johnson lambda per input column, uses lambda = 1 for constant columns, and applies the transform. - powerTransformApply.dml applies the Yeo-Johnson transform with supplied per-column lambdas. Register both builtins so they can be resolved by SystemDS. Add powerTransformSmokeTest.dml to verify: - powerTransformApply with lambda = 1 behaves as identity - powerTransform returns output dimensions matching the input - one lambda is returned per input column - constant columns use lambda = 1 - transformed output and lambdas do not contain NaN or Inf The smoke test passes with: ./bin/systemds src/test/scripts/functions/builtin/powerTransformSmokeTest.dml
Add a focused PowerTransformApply test that compares the DML builtin against
an independent R reference implementation.
The test adds:
- powerTransformApply.dml as a small DML wrapper around the registered builtin
- powerTransformApply.R as the reference Yeo-Johnson apply implementation
- BuiltinPowerTransformTest.java to run the DML and R scripts and compare Y
The test covers the key apply branches with fixed lambdas:
- lambda = 0 for the positive log branch
- lambda = 1 for the identity-style middle case
- lambda = 2 for the negative log branch
Also translate the remaining PowerTransformer comments from Chinese to English
in the prototype DML files.
Verified with:
Rscript -e 'parse(file="src/test/scripts/functions/builtin/powerTransformApply.R"); cat("R syntax OK\n")'
./bin/systemds src/test/scripts/functions/builtin/powerTransformSmokeTest.dml
mvn -Dtest=BuiltinPowerTransformTest test
130c741 to
753c5a0
Compare
gaturchenko
left a comment
There was a problem hiding this comment.
Hey @WenliangCao, thank you very much for the contribution and extensive experiments. I have left some comments, please address them so that we can merge your code into the main. The main issue however is a failing test, we cannot merge if the CI is red, so please address it first. Feel free to reach out via an email or right here in the PR thread if anything is unclear or if you disagree with some of my comments, we can absolutely discuss anything.
|
|
||
| # Apply one transform with this lambda and use the temporary y for scoring | ||
| emptyStats = matrix(0.0, rows=0, cols=0) | ||
| y = powerTransformApply(x, lambdaMatrix, emptyStats, emptyStats, method); |
There was a problem hiding this comment.
This has a fixed invocation overhead, and for matrices with, say, 1000 rows and 4 columns, this will dominate the runtime, so it should be inlined somehow
|
|
||
| # Jacobian term for the selected transformation | ||
| if (method == "box-cox") { | ||
| jacobian = (lambda - 1.0) * sum(log(x)); |
There was a problem hiding this comment.
Only lambda changes here, x is always the same, so you can compute sum(log(x)) just once, which can be done with a separate function. Then you can add something like jacTerm as another input to ptNegLogLikelihood and get jacobian = (lambda - 1.0) * jacTerm;. This should improve performance substantially for large matrices
| jacobian = (lambda - 1.0) * sum(log(x)); | ||
| } | ||
| else { | ||
| jacobian = (lambda - 1.0) * sum(sign(x) * log(abs(x) + 1.0)); |
There was a problem hiding this comment.
Same logic as for the comment above
|
|
||
| private void runPowerTransformTest( | ||
| String method, boolean standardize, double[][] input, boolean shouldFail) { | ||
| ExecMode oldExecMode = setExecMode(ExecType.CP); |
There was a problem hiding this comment.
You only add CP coverage, would be good to have Spark covered as well. Let us know if there are particular issues with Spark for Power Transform however
| } | ||
|
|
||
| @Test | ||
| public void testPowerTransformYeoJohnsonLambdaAboveInitialInterval() { |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Add the required blank line after the Apache license headers in the PowerTransformer fit and apply built-ins. Remove the redundant -exec singlenode arguments from the fit and apply tests, which already select the execution mode through the test framework. Apply dev/format-changed.sh against upstream/main to format PR-edited Java lines with dev/CodeStyle_eclipse.xml and address the Java Format Check failure.
|
In addition the format check fails @WenliangCao, please run |
a9cde52 to
5df732f
Compare
5df732f to
280e878
Compare
| # X Input feature matrix of shape n-by-m | ||
| # method Power transformation method: "yeo-johnson" (default) or "box-cox" | ||
| # standardize Whether to normalize transformed columns to zero mean and unit variance |
There was a problem hiding this comment.
Please, remove 2 spaces after # such that you have 1 space in between # and the text that follows
| # Y Power-transformed matrix of shape n-by-m | ||
| # lambdas Estimated lambda parameters of shape 1-by-m, one per column | ||
| # means Transformed column means of shape 1-by-m, or an empty matrix when not standardized | ||
| # scales Transformed column scales of shape 1-by-m, or an empty matrix when not standardized |
There was a problem hiding this comment.
Please, remove 2 spaces after # such that you have 1 space in between # and the text that follows
| # X Input feature matrix of shape n-by-m | ||
| # lambdas Precomputed lambda parameters of shape 1-by-m, one per column | ||
| # means Transformed column means of shape 1-by-m; empty to skip standardization | ||
| # scales Transformed column scales of shape 1-by-m; empty to skip standardization | ||
| # method Power transformation method: "yeo-johnson" (default) or "box-cox" |
There was a problem hiding this comment.
Please, remove 2 spaces after # such that you have 1 space in between # and the text that follows
| # | ||
| # OUTPUT: | ||
| # ------------------------------------------------------------------------------------- | ||
| # Y Power-transformed matrix of shape n-by-m |
There was a problem hiding this comment.
Please, remove 2 spaces after # such that you have 1 space in between # and the text that follows
| if (sum(is.na(y)) > 0 | sum(is.infinite(y)) > 0 | | ||
| is.na(yVariance) | is.infinite(yVariance) | yVariance <= 0.0) { |
There was a problem hiding this comment.
Just if (is.na(yVariance) | is.infinite(yVariance) | yVariance <= 0.0) will suffice
|
Hey @WenliangCao, thank you very much for addressing the initial review, you've done a very good job. There a few more minor things to resolve, I'd appreciate if you could look into it. Once it's done, we're good to merge your code. |
Hi Grigorii, thank you very much for taking the time to review my code and for your helpful guidance. I have addressed the remaining comments and pushed the updates. I hope everything looks good now, and please let me know if any further adjustments are needed. |
Thank you for addressing these. Currently, the code style check is failing, please run |
Hi Grigorii @gaturchenko , after checking the details of the Java Format Check, I found that it used base SHA 759d5b6 from August 4. The files reported as incorrectly formatted were therefore introduced by later changes on main, rather than by my PR. I tried synchronizing my branch with the current main tip, 9dccbc3 from August 8, and reran dev/format-changed.sh locally. The local format check now passes, and I hope the cloud CI will pass this time as well. |
Summary
This pull request adds PowerTransformer built-ins with separate fit and apply workflows. The implementation supports Yeo-Johnson and Box-Cox transformations, per-column parameter estimation, optional standardization, and reuse of fitted parameters on new data.
Yeo-Johnson is the default method and supports mixed-sign inputs. Box-Cox is available for strictly positive inputs.
API
powerTransformestimates one lambda per column and optionally standardizes the transformed columns.powerTransformApplyreuses the fitted lambdas, means, and scales without re-estimation.Implementation
powerTransform.dmlfor fitting and transforming feature matrices.powerTransformApply.dmlfor applying fitted transformations.[-2, 2]as an initial bracket and expand it when necessary, allowing the optimum to lie outside the initial interval.Validation
mvn -Dtest=BuiltinPowerTransformTest testThe tests cover:
[-2, 2]interval;Fitted Yeo-Johnson and Box-Cox outputs are compared against the established R
recipespackage.Documentation
User-facing signatures, arguments, return values, input requirements, and examples are documented in
docs/site/builtins-reference.md.Experiments
The reproducible evaluation compares no scaling, standard scaling, robust scaling, Yeo-Johnson, and Box-Cox on nine public datasets. Each experiment uses the fixed seeds
13,37,73,101, and149. All raw results, summaries, figures, the workbook, and the report were regenerated with the final implementation.n_init=20)Supervised tasks use 80/20 train/test splits; classification is stratified and Parkinsons Telemonitoring uses subject-level group splits. Each transformation is fitted on training data and applied to test data. Clustering uses the full dataset.
The table reports the mean primary metric over five runs. A dash indicates that Box-Cox is not applicable because the feature matrix contains non-positive values.
Power transformations rank first or tie for first on four of the nine datasets. Representative results include:
0.6486with standard scaling to0.6114.0.9618.0.7759.0.5330, compared with0.1873for standard scaling.Reproducible artifacts
Limitations
The downstream effect of preprocessing is dataset- and estimator-dependent. The evaluation uses fixed distance-based estimators, three datasets per task, and no hyperparameter search or statistical significance test. The results therefore demonstrate practical cases where power transformations help, rather than universal superiority over other preprocessing methods.
Related issue
SYSTEMDS-3863