|
24 | 24 | import bigframes_vendored.sklearn.preprocessing._discretization |
25 | 25 | import bigframes_vendored.sklearn.preprocessing._encoder |
26 | 26 | import bigframes_vendored.sklearn.preprocessing._label |
| 27 | +import bigframes_vendored.sklearn.preprocessing._polynomial |
27 | 28 |
|
28 | 29 | from bigframes.core import log_adapter |
29 | 30 | from bigframes.ml import base, core, globals, utils |
@@ -661,6 +662,109 @@ def transform(self, y: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: |
661 | 662 | ) |
662 | 663 |
|
663 | 664 |
|
| 665 | +@log_adapter.class_logger |
| 666 | +class PolynomialFeatures( |
| 667 | + base.Transformer, |
| 668 | + bigframes_vendored.sklearn.preprocessing._polynomial.PolynomialFeatures, |
| 669 | +): |
| 670 | + __doc__ = ( |
| 671 | + bigframes_vendored.sklearn.preprocessing._polynomial.PolynomialFeatures.__doc__ |
| 672 | + ) |
| 673 | + |
| 674 | + def __init__(self, degree: int = 2): |
| 675 | + self.degree = degree |
| 676 | + self._bqml_model: Optional[core.BqmlModel] = None |
| 677 | + self._bqml_model_factory = globals.bqml_model_factory() |
| 678 | + self._base_sql_generator = globals.base_sql_generator() |
| 679 | + |
| 680 | + # TODO(garrettwu): implement __hash__ |
| 681 | + def __eq__(self, other: Any) -> bool: |
| 682 | + return ( |
| 683 | + type(other) is PolynomialFeatures and self._bqml_model == other._bqml_model |
| 684 | + ) |
| 685 | + |
| 686 | + def _compile_to_sql(self, columns: List[str], X=None) -> List[Tuple[str, str]]: |
| 687 | + """Compile this transformer to a list of SQL expressions that can be included in |
| 688 | + a BQML TRANSFORM clause |
| 689 | +
|
| 690 | + Args: |
| 691 | + columns: |
| 692 | + a list of column names to transform. |
| 693 | + X (default None): |
| 694 | + Ignored. |
| 695 | +
|
| 696 | + Returns: a list of tuples of (sql_expression, output_name)""" |
| 697 | + output_name = "poly_feat" |
| 698 | + return [ |
| 699 | + ( |
| 700 | + self._base_sql_generator.ml_polynomial_expand( |
| 701 | + columns, self.degree, output_name |
| 702 | + ), |
| 703 | + output_name, |
| 704 | + ) |
| 705 | + ] |
| 706 | + |
| 707 | + @classmethod |
| 708 | + def _parse_from_sql(cls, sql: str) -> tuple[PolynomialFeatures, str]: |
| 709 | + """Parse SQL to tuple(PolynomialFeatures, column_label). |
| 710 | +
|
| 711 | + Args: |
| 712 | + sql: SQL string of format "ML.POLYNOMIAL_EXPAND(STRUCT(col_label0, col_label1, ...), degree)" |
| 713 | +
|
| 714 | + Returns: |
| 715 | + tuple(MaxAbsScaler, column_label)""" |
| 716 | + col_label = sql[sql.find("STRUCT(") + 7 : sql.find(")")] |
| 717 | + degree = int(sql[sql.rfind(",") + 1 : sql.rfind(")")]) |
| 718 | + return cls(degree), col_label |
| 719 | + |
| 720 | + def fit( |
| 721 | + self, |
| 722 | + X: Union[bpd.DataFrame, bpd.Series], |
| 723 | + y=None, # ignored |
| 724 | + ) -> PolynomialFeatures: |
| 725 | + (X,) = utils.convert_to_dataframe(X) |
| 726 | + |
| 727 | + compiled_transforms = self._compile_to_sql(X.columns.tolist()) |
| 728 | + transform_sqls = [transform_sql for transform_sql, _ in compiled_transforms] |
| 729 | + |
| 730 | + self._bqml_model = self._bqml_model_factory.create_model( |
| 731 | + X, |
| 732 | + options={"model_type": "transform_only"}, |
| 733 | + transforms=transform_sqls, |
| 734 | + ) |
| 735 | + |
| 736 | + # TODO(garrettwu): generalize the approach to other transformers |
| 737 | + output_names = [] |
| 738 | + for transform_col in self._bqml_model._model._properties["transformColumns"]: |
| 739 | + transform_col_dict = cast(dict, transform_col) |
| 740 | + # pass the columns that are not transformed |
| 741 | + if "transformSql" not in transform_col_dict: |
| 742 | + continue |
| 743 | + transform_sql: str = transform_col_dict["transformSql"] |
| 744 | + if not transform_sql.startswith("ML."): |
| 745 | + continue |
| 746 | + |
| 747 | + output_names.append(transform_col_dict["name"]) |
| 748 | + |
| 749 | + self._output_names = output_names |
| 750 | + |
| 751 | + return self |
| 752 | + |
| 753 | + def transform(self, X: Union[bpd.DataFrame, bpd.Series]) -> bpd.DataFrame: |
| 754 | + if not self._bqml_model: |
| 755 | + raise RuntimeError("Must be fitted before transform") |
| 756 | + |
| 757 | + (X,) = utils.convert_to_dataframe(X) |
| 758 | + |
| 759 | + df = self._bqml_model.transform(X) |
| 760 | + return typing.cast( |
| 761 | + bpd.DataFrame, |
| 762 | + df[self._output_names], |
| 763 | + ) |
| 764 | + |
| 765 | + # TODO(garrettwu): to_gbq() |
| 766 | + |
| 767 | + |
664 | 768 | PreprocessingType = Union[ |
665 | 769 | OneHotEncoder, |
666 | 770 | StandardScaler, |
|
0 commit comments