@@ -703,33 +703,44 @@ def intersect_plane(self, other: Plane, **kwargs) -> Line:
703703 return Line (point_line , direction_line )
704704
705705 @classmethod
706- def best_fit (cls , points : array_like , tol : Optional [float ] = None , ** kwargs ) -> Plane :
706+ def best_fit (
707+ cls ,
708+ points : array_like ,
709+ tol : Optional [float ] = None ,
710+ return_error : bool = False ,
711+ ** kwargs ,
712+ ) -> Plane | tuple [Plane , float ]:
707713 """
708714 Return the plane of best fit for a set of 3D points.
709715
716+ Also optionally return a value representing the error of the fit.
717+ This is the sum of the squared singular values from SVD (excluding the first two).
718+
719+ "The singular values reflect the amount of data variance captured by the bases.
720+ The first basis (the one with largest singular value) lies in the direction of the greatest data variance.
721+ The second basis captures the orthogonal direction with the second greatest variance, and so on." [1]_
722+
710723 Parameters
711724 ----------
712725 points : array_like
713726 Input 3D points.
714727 tol : float | None, optional
715728 Keyword passed to :meth:`Points.are_collinear` (default None).
729+ return_error : bool, optional
730+ If True, also return a value representing the error of the fit (default False).
716731 kwargs : dict, optional
717732 Additional keywords passed to :func:`numpy.linalg.svd`
718733
719734 Returns
720735 -------
721- Plane
722- The plane of best fit.
736+ Plane | tuple[Plane, float]
737+ The plane of best fit, and optionally the error of the fit .
723738
724739 Raises
725740 ------
726741 ValueError
727742 If the points are collinear or are not 3D.
728743
729- References
730- ----------
731- https://scicomp.stackexchange.com/a/6901
732-
733744 Examples
734745 --------
735746 >>> from skspatial.objects import Plane
@@ -755,6 +766,11 @@ def best_fit(cls, points: array_like, tol: Optional[float] = None, **kwargs) ->
755766 >>> Plane.best_fit(points, full_matrices=False)
756767 Plane(point=Point([0.5, 0.5, 0. ]), normal=Vector([0., 0., 1.]))
757768
769+ References
770+ ----------
771+ .. [1] : "Singular Value Decomposition", Oracle, https://docs.oracle.com/en/database/oracle/machine-learning/oml4sql/23/dmcon/singular-value-decomposition.html#GUID-14AA4B45-3B36-4056-9B9A-BD9DC471F0AD
772+ .. [2] : https://scicomp.stackexchange.com/a/6901
773+
758774 """
759775 points = Points (points )
760776
@@ -766,10 +782,16 @@ def best_fit(cls, points: array_like, tol: Optional[float] = None, **kwargs) ->
766782
767783 points_centered , centroid = points .mean_center (return_centroid = True )
768784
769- u , _ , _ = np .linalg .svd (points_centered .T , ** kwargs )
770- normal = Vector (u [:, 2 ])
785+ U , S , _ = np .linalg .svd (points_centered .T , ** kwargs )
786+ normal = Vector (U [:, 2 ])
787+
788+ plane_fit = cls (centroid , normal )
789+
790+ if return_error :
791+ error_fit = np .sum (S [2 :] ** 2 )
792+ return plane_fit , error_fit
771793
772- return cls ( centroid , normal )
794+ return plane_fit
773795
774796 def to_mesh (
775797 self ,
0 commit comments