Coverage for pygeodesy/fstats.py: 99%
307 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-02 14:35 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-02 14:35 -0400
2# -*- coding: utf-8 -*-
4u'''Classes for I{running} statistics and regressions based on
5L{pygeodesy.Fsum}, precision floating point summation.
6'''
7# make sure int/int division yields float quotient, see .basics
8from __future__ import division as _; del _ # PYCHOK semicolon
10from pygeodesy.basics import isodd, islistuple, _xinstanceof, \
11 _xsubclassof, _zip
12from pygeodesy.constants import _0_0, _2_0, _3_0, _4_0, _6_0, _xError
13# from pygeodesy.errors import _xError # from .constants
14from pygeodesy.fmath import hypot2, sqrt
15from pygeodesy.fsums import _2float, Fsum, _iadd_op_, Fmt
16from pygeodesy.interns import NN, _invalid_, _other_, _SPACE_
17from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY
18from pygeodesy.named import _Named, _NotImplemented, property_RO
19# from pygeodesy.props import property_RO # from .named
20# from pygeodesy.streprs import Fmt # from .fsums
22# from math import sqrt # from .fmath
24__all__ = _ALL_LAZY.fstats
25__version__ = '24.04.26'
27_Floats = Fsum, float
28_Scalar = _Floats + (int,) # XXX basics._Ints is ABCMeta in Python 2
29try:
30 _Scalar += (long,)
31except NameError: # Python 3+
32 pass
35def _2Floats(xs, ys=False):
36 '''(INTERNAL) Yield each value as C{float} or L{Fsum}.
37 '''
38 if ys:
39 def _2f(i, x):
40 return _2float(index=i, ys=x)
41 else:
42 def _2f(i, x): # PYCHOK redef
43 return _2float(index=i, xs=x)
45 for i, x in enumerate(xs): # don't unravel Fsums
46 yield x if isinstance(x, _Floats) else _2f(i, x)
49def _sampled(n, sample):
50 '''(INTERNAL) Return the sample or the entire count.
51 '''
52 return (n - 1) if sample and n > 0 else n
55class _FstatsNamed(_Named):
56 '''(INTERNAL) Base class.
57 '''
58 _n = 0
60 def __add__(self, other):
61 '''Sum of this and a scalar, an L{Fsum} or an other instance.
62 '''
63 f = self.fcopy(name=self.__add__.__name__) # PYCHOK expected
64 f += other
65 return f
67 def __float__(self): # PYCHOK no cover
68 '''Not implemented.'''
69 return _NotImplemented(self)
71 def __int__(self): # PYCHOK no cover
72 '''Not implemented.'''
73 return _NotImplemented(self)
75 def __len__(self):
76 '''Return the I{total} number of accumulated values (C{int}).
77 '''
78 return self._n
80 def __neg__(self): # PYCHOK no cover
81 '''Not implemented.'''
82 return _NotImplemented(self)
84 def __radd__(self, other): # PYCHOK no cover
85 '''Not implemented.'''
86 return _NotImplemented(self, other)
88 def __str__(self):
89 return Fmt.SQUARE(self.named3, len(self))
91 def fcopy(self, deep=False, name=NN):
92 '''Copy this instance, C{shallow} or B{C{deep}}.
93 '''
94 n = name or self.fcopy.__name__
95 f = _Named.copy(self, deep=deep, name=n)
96 return self._copy(f, self) # PYCHOK expected
98 copy = fcopy
101class _FstatsBase(_FstatsNamed):
102 '''(INTERNAL) Base running stats class.
103 '''
104 _Ms = ()
106 def _copy(self, c, s):
107 '''(INTERNAL) Copy C{B{c} = B{s}}.
108 '''
109 _xinstanceof(self.__class__, c=c, s=s)
110 c._Ms = tuple(M.fcopy() for M in s._Ms) # deep=False
111 c._n = s._n
112 return c
114 def fadd(self, xs, sample=False): # PYCHOK no cover
115 '''I{Must be overloaded}.'''
116 self._notOverloaded(xs, sample=sample)
118 def fadd_(self, *xs, **sample):
119 '''Accumulate and return the current count.
121 @see: Method C{fadd}.
122 '''
123 return self.fadd(xs, **sample)
125 def fmean(self, xs=None):
126 '''Accumulate and return the current mean.
128 @kwarg xs: Iterable with additional values (C{Scalar}s).
130 @return: Current, running mean (C{float}).
132 @see: Method C{fadd}.
133 '''
134 if xs:
135 self.fadd(xs)
136 return self._M1.fsum()
138 def fmean_(self, *xs):
139 '''Accumulate and return the current mean.
141 @see: Method C{fmean}.
142 '''
143 return self.fmean(xs)
145 def fstdev(self, xs=None, sample=False):
146 '''Accumulate and return the current standard deviation.
148 @kwarg xs: Iterable with additional values (C{Scalar}).
149 @kwarg sample: Return the I{sample} instead of the entire
150 I{population} standard deviation (C{bool}).
152 @return: Current, running (sample) standard deviation (C{float}).
154 @see: Method C{fadd}.
155 '''
156 v = self.fvariance(xs, sample=sample)
157 return sqrt(v) if v > 0 else _0_0
159 def fstdev_(self, *xs, **sample):
160 '''Accumulate and return the current standard deviation.
162 @see: Method C{fstdev}.
163 '''
164 return self.fstdev(xs, **sample)
166 def fvariance(self, xs=None, sample=False):
167 '''Accumulate and return the current variance.
169 @kwarg xs: Iterable with additional values (C{Scalar}s).
170 @kwarg sample: Return the I{sample} instead of the entire
171 I{population} variance (C{bool}).
173 @return: Current, running (sample) variance (C{float}).
175 @see: Method C{fadd}.
176 '''
177 n = self.fadd(xs, sample=sample)
178 return float(self._M2 / float(n)) if n > 0 else _0_0
180 def fvariance_(self, *xs, **sample):
181 '''Accumulate and return the current variance.
183 @see: Method C{fvariance}.
184 '''
185 return self.fvariance(xs, **sample)
187 def _iadd_other(self, other):
188 '''(INTERNAL) Add Scalar or Scalars.
189 '''
190 if isinstance(other, _Scalar):
191 self.fadd_(other)
192 else:
193 try:
194 if not islistuple(other):
195 raise TypeError(_SPACE_(_invalid_, _other_))
196 self.fadd(other)
197 except Exception as x:
198 raise _xError(x, _SPACE_(self, _iadd_op_, repr(other)))
200 @property_RO
201 def _M1(self):
202 '''(INTERNAL) get the 1st Moment accumulator.'''
203 return self._Ms[0]
205 @property_RO
206 def _M2(self):
207 '''(INTERNAL) get the 2nd Moment accumulator.'''
208 return self._Ms[1]
211class Fcook(_FstatsBase):
212 '''U{Cook<https://www.JohnDCook.com/blog/skewness_kurtosis>}'s
213 C{RunningStats} computing the running mean, median and
214 (sample) kurtosis, skewness, variance, standard deviation
215 and Jarque-Bera normality.
217 @see: L{Fwelford} and U{Higher-order statistics<https://
218 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}.
219 '''
220 def __init__(self, xs=None, name=NN):
221 '''New L{Fcook} stats accumulator.
223 @kwarg xs: Iterable with initial values (C{Scalar}s).
224 @kwarg name: Optional name (C{str}).
226 @see: Method L{Fcook.fadd}.
227 '''
228 self._Ms = tuple(Fsum() for _ in range(4)) # 1st, 2nd ... Moment
229 if name:
230 self.name = name
231 if xs:
232 self.fadd(xs)
234 def __iadd__(self, other):
235 '''Add B{C{other}} to this L{Fcook} instance.
237 @arg other: An L{Fcook} instance or C{Scalar}s, meaning
238 one or more C{scalar} or L{Fsum} instances.
240 @return: This instance, updated (L{Fcook}).
242 @raise TypeError: Invalid B{C{other}} type.
244 @raise ValueError: Invalid B{C{other}}.
246 @see: Method L{Fcook.fadd}.
247 '''
248 if isinstance(other, Fcook):
249 nb = len(other)
250 if nb > 0:
251 na = len(self)
252 if na > 0:
253 A1, A2, A3, A4 = self._Ms
254 B1, B2, B3, B4 = other._Ms
256 n = na + nb
257 n_ = float(n)
258 D = A1 - B1 # b1 - a1
259 Dn = D / n_
260 Dn2 = Dn**2 # d**2 / n**2
261 nab = na * nb
262 Dn3 = Dn2 * (D * nab)
264 na2 = na**2
265 nb2 = nb**2
266 A4 += B4
267 A4 += (B3 * na - (A3 * nb)) * (Dn * _4_0)
268 A4 += (B2 * na2 + (A2 * nb2)) * (Dn2 * _6_0)
269 A4 += (Dn * Dn3) * (na2 - nab + nb2) # d**4 / n**3
271 A3 += B3
272 A3 += (A2 * na - (B2 * nb)) * (Dn * _3_0)
273 A3 += Dn3 * (na - nb)
275 A2 += B2
276 A2 += Dn2 * (nab / n_)
278 B1n = B1 * nb # if other is self
279 A1 *= na
280 A1 += B1n
281 A1 *= 1 / n_ # /= chokes PyChecker
283# self._Ms = A1, A2, A3, A4
284 self._n = n
285 else:
286 self._copy(self, other)
287 else:
288 self._iadd_other(other)
289 return self
291 def fadd(self, xs, sample=False):
292 '''Accumulate and return the current count.
294 @arg xs: Iterable with additional values (C{Scalar}s,
295 meaning C{scalar} or L{Fsum} instances).
296 @kwarg sample: Return the I{sample} instead of the entire
297 I{population} count (C{bool}).
299 @return: Current, running (sample) count (C{int}).
301 @raise OverflowError: Partial C{2sum} overflow.
303 @raise TypeError: Non-scalar B{C{xs}} value.
305 @raise ValueError: Invalid or non-finite B{C{xs}} value.
307 @see: U{online_kurtosis<https://WikiPedia.org/wiki/
308 Algorithms_for_calculating_variance>}.
309 '''
310 n = self._n
311 if xs:
312 M1, M2, M3, M4 = self._Ms
313 for x in _2Floats(xs):
314 n1 = n
315 n += 1
316 D = x - M1
317 Dn = D / n
318 if Dn:
319 Dn2 = Dn**2
320 if n1 > 1:
321 T1 = D * (Dn * n1)
322 T2 = T1 * (Dn * (n1 - 1))
323 T3 = T1 * (Dn2 * (n**2 - 3 * n1))
324 elif n1 > 0: # n1 == 1, n == 2
325 T1 = D * Dn
326 T2 = _0_0
327 T3 = T1 * Dn2
328 else:
329 T1 = T2 = T3 = _0_0
330 M4 += T3
331 M4 -= M3 * (Dn * _4_0)
332 M4 += M2 * (Dn2 * _6_0)
334 M3 += T2
335 M3 -= M2 * (Dn * _3_0)
337 M2 += T1
338 M1 += Dn
339# self._Ms = M1, M2, M3, M4
340 self._n = n
341 return _sampled(n, sample)
343 def fjb(self, xs=None, sample=True, excess=True):
344 '''Accumulate and compute the current U{Jarque-Bera
345 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality.
347 @kwarg xs: Iterable with additional values (C{Scalar}s).
348 @kwarg sample: Return the I{sample} normality (C{bool}), default.
349 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default.
351 @return: Current, running (sample) Jarque-Bera normality (C{float}).
353 @see: Method L{Fcook.fadd}.
354 '''
355 n = self.fadd(xs, sample=sample)
356 k = self.fkurtosis(sample=sample, excess=excess) / _2_0
357 s = self.fskewness(sample=sample)
358 return n * hypot2(k, s) / _6_0
360 def fjb_(self, *xs, **sample_excess):
361 '''Accumulate and compute the current U{Jarque-Bera
362 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality.
364 @see: Method L{Fcook.fjb}.
365 '''
366 return self.fjb(xs, **sample_excess)
368 def fkurtosis(self, xs=None, sample=False, excess=True):
369 '''Accumulate and return the current kurtosis.
371 @kwarg xs: Iterable with additional values (C{Scalar}s).
372 @kwarg sample: Return the I{sample} instead of the entire
373 I{population} kurtosis (C{bool}).
374 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default.
376 @return: Current, running (sample) kurtosis or I{excess} kurtosis (C{float}).
378 @see: U{Kurtosis Formula<https://www.Macroption.com/kurtosis-formula>}
379 and U{Mantalos<https://www.ResearchGate.net/publication/227440210>}.
381 @see: Method L{Fcook.fadd}.
382 '''
383 k, n = _0_0, self.fadd(xs, sample=sample)
384 if n > 0:
385 _, M2, _, M4 = self._Ms
386 m2 = float(M2 * M2)
387 if m2:
388 K, x = (M4 * (n / m2)), _3_0
389 if sample and 2 < n < len(self):
390 d = float((n - 1) * (n - 2))
391 K *= (n + 1) * (n + 2) / d
392 x *= n**2 / d
393 if excess:
394 K -= x
395 k = K.fsum()
396 return k
398 def fkurtosis_(self, *xs, **sample_excess):
399 '''Accumulate and return the current kurtosis.
401 @see: Method L{Fcook.fkurtosis}.
402 '''
403 return self.fkurtosis(xs, **sample_excess)
405 def fmedian(self, xs=None):
406 '''Accumulate and return the current median.
408 @kwarg xs: Iterable with additional values (C{Scalar}s).
410 @return: Current, running median (C{float}).
412 @see: U{Pearson's Skewness Coefficients<https://MathWorld.Wolfram.com/
413 PearsonsSkewnessCoefficients.html>}, U{Skewness & Kurtosis Simplified
414 https://TowardsDataScience.com/skewness-kurtosis-simplified-1338e094fc85>}
415 and method L{Fcook.fadd}.
416 '''
417 # skewness = 3 * (mean - median) / stdev, i.e.
418 # median = mean - skewness * stdef / 3
419 m = float(self._M1) if xs is None else self.fmean(xs)
420 return m - self.fskewness() * self.fstdev() / _3_0
422 def fmedian_(self, *xs):
423 '''Accumulate and return the current median.
425 @see: Method L{Fcook.fmedian}.
426 '''
427 return self.fmedian(xs)
429 def fskewness(self, xs=None, sample=False):
430 '''Accumulate and return the current skewness.
432 @kwarg xs: Iterable with additional values (C{Scalar}s).
433 @kwarg sample: Return the I{sample} instead of the entire
434 I{population} skewness (C{bool}).
436 @return: Current, running (sample) skewness (C{float}).
438 @see: U{Skewness Formula<https://www.Macroption.com/skewness-formula/>}
439 and U{Mantalos<https://www.ResearchGate.net/publication/227440210>}.
441 @see: Method L{Fcook.fadd}.
442 '''
443 s, n = _0_0, self.fadd(xs, sample=sample)
444 if n > 0:
445 _, M2, M3, _ = self._Ms
446 m = float(M2**3)
447 if m > 0:
448 S = M3 * sqrt(float(n) / m)
449 if sample and 1 < n < len(self):
450 S *= (n + 1) / float(n - 1)
451 s = S.fsum()
452 return s
454 def fskewness_(self, *xs, **sample):
455 '''Accumulate and return the current skewness.
457 @see: Method L{Fcook.fskewness}.
458 '''
459 return self.fskewness(xs, **sample)
461 def toFwelford(self, name=NN):
462 '''Return an L{Fwelford} equivalent.
463 '''
464 f = Fwelford(name=name or self.name)
465 f._Ms = self._M1.fcopy(), self._M2.fcopy() # deep=False
466 f._n = self._n
467 return f
470class Fwelford(_FstatsBase):
471 '''U{Welford<https://WikiPedia.org/wiki/Algorithms_for_calculating_variance>}'s
472 accumulator computing the running mean, (sample) variance and standard deviation.
474 @see: U{Cook<https://www.JohnDCook.com/blog/standard_deviation/>} and L{Fcook}.
475 '''
476 def __init__(self, xs=None, name=NN):
477 '''New L{Fwelford} stats accumulator.
479 @kwarg xs: Iterable with initial values (C{Scalar}s).
480 @kwarg name: Optional name (C{str}).
482 @see: Method L{Fwelford.fadd}.
483 '''
484 self._Ms = Fsum(), Fsum() # 1st and 2nd Moment
485 if name:
486 self.name = name
487 if xs:
488 self.fadd(xs)
490 def __iadd__(self, other):
491 '''Add B{C{other}} to this L{Fwelford} instance.
493 @arg other: An L{Fwelford} or L{Fcook} instance or C{Scalar}s,
494 meaning one or more C{scalar} or L{Fsum} instances.
496 @return: This instance, updated (L{Fwelford}).
498 @raise TypeError: Invalid B{C{other}} type.
500 @raise ValueError: Invalid B{C{other}}.
502 @see: Method L{Fwelford.fadd} and U{Parallel algorithm<https//
503 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}.
504 '''
505 if isinstance(other, Fwelford):
506 nb = len(other)
507 if nb > 0:
508 na = len(self)
509 if na > 0:
510 M, S = self._Ms
511 M_, S_ = other._Ms
513 n = na + nb
514 n_ = float(n)
516 D = M_ - M
517 D *= D # D**2
518 D *= na * nb / n_
519 S += D
520 S += S_
522 Mn = M_ * nb # if other is self
523 M *= na
524 M += Mn
525 M *= 1 / n_ # /= chokes PyChecker
527# self._Ms = M, S
528 self._n = n
529 else:
530 self._copy(self, other)
532 elif isinstance(other, Fcook):
533 self += other.toFwelford()
534 else:
535 self._iadd_other(other)
536 return self
538 def fadd(self, xs, sample=False):
539 '''Accumulate and return the current count.
541 @arg xs: Iterable with additional values (C{Scalar}s,
542 meaning C{scalar} or L{Fsum} instances).
543 @kwarg sample: Return the I{sample} instead of the entire
544 I{population} count (C{bool}).
546 @return: Current, running (sample) count (C{int}).
548 @raise OverflowError: Partial C{2sum} overflow.
550 @raise TypeError: Non-scalar B{C{xs}} value.
552 @raise ValueError: Invalid or non-finite B{C{xs}} value.
553 '''
554 n = self._n
555 if xs:
556 M, S = self._Ms
557 for x in _2Floats(xs):
558 n += 1
559 D = x - M
560 M += D / n
561 D *= x - M
562 S += D
563# self._Ms = M, S
564 self._n = n
565 return _sampled(n, sample)
568class Flinear(_FstatsNamed):
569 '''U{Cook<https://www.JohnDCook.com/blog/running_regression>}'s
570 C{RunningRegression} computing the running slope, intercept
571 and correlation of a linear regression.
572 '''
573 def __init__(self, xs=None, ys=None, Fstats=Fwelford, name=NN):
574 '''New L{Flinear} regression accumulator.
576 @kwarg xs: Iterable with initial C{x} values (C{Scalar}s).
577 @kwarg ys: Iterable with initial C{y} values (C{Scalar}s).
578 @kwarg Fstats: Stats class for C{x} and C{y} values (L{Fcook}
579 or L{Fwelford}).
580 @kwarg name: Optional name (C{str}).
582 @raise TypeError: Invalid B{C{Fs}}, not L{Fcook} or
583 L{Fwelford}.
584 @see: Method L{Flinear.fadd}.
585 '''
586 _xsubclassof(Fcook, Fwelford, Fstats=Fstats)
587 if name:
588 self.name = name
590 self._S = Fsum(name=name)
591 self._X = Fstats(name=name)
592 self._Y = Fstats(name=name)
593 if xs and ys:
594 self.fadd(xs, ys)
596 def __iadd__(self, other):
597 '''Add B{C{other}} to this instance.
599 @arg other: An L{Flinear} instance or C{Scalar} pairs,
600 meaning C{scalar} or L{Fsum} instances.
602 @return: This instance, updated (L{Flinear}).
604 @raise TypeError: Invalid B{C{other}} or the B{C{other}}
605 and these C{x} and C{y} accumulators
606 are not compatible.
608 @raise ValueError: Invalid or odd-length B{C{other}}.
610 @see: Method L{Flinear.fadd_}.
611 '''
612 if isinstance(other, Flinear):
613 if len(other) > 0:
614 if len(self) > 0:
615 n = other._n
616 S = other._S
617 X = other._X
618 Y = other._Y
619 D = (X._M1 - self._X._M1) * \
620 (Y._M1 - self._Y._M1) * \
621 (n * self._n / float(n + self._n))
622 self._n += n
623 self._S += S + D
624 self._X += X
625 self._Y += Y
626 else:
627 self._copy(self, other)
628 else:
629 try:
630 if not islistuple(other):
631 raise TypeError(_SPACE_(_invalid_, _other_))
632 elif isodd(len(other)):
633 raise ValueError(Fmt.PAREN(isodd=Fmt.PAREN(len=_other_)))
634 self.fadd_(*other)
635 except Exception as x:
636 raise _xError(x, _SPACE_(self, _iadd_op_, repr(other)))
637 return self
639 def _copy(self, c, s):
640 '''(INTERNAL) Copy C{B{c} = B{s}}.
641 '''
642 _xinstanceof(Flinear, c=c, s=s)
643 c._n = s._n
644 c._S = s._S.fcopy(deep=False)
645 c._X = s._X.fcopy(deep=False)
646 c._Y = s._Y.fcopy(deep=False)
647 return c
649 def fadd(self, xs, ys, sample=False):
650 '''Accumulate and return the current count.
652 @arg xs: Iterable with additional C{x} values (C{Scalar}s),
653 meaning C{scalar} or L{Fsum} instances).
654 @arg ys: Iterable with additional C{y} values (C{Scalar}s,
655 meaning C{scalar} or L{Fsum} instances).
656 @kwarg sample: Return the I{sample} instead of the entire
657 I{population} count (C{bool}).
659 @return: Current, running (sample) count (C{int}).
661 @raise OverflowError: Partial C{2sum} overflow.
663 @raise TypeError: Non-scalar B{C{xs}} or B{C{ys}} value.
665 @raise ValueError: Invalid or non-finite B{C{xs}} or B{C{ys}} value.
666 '''
667 n = self._n
668 if xs and ys:
669 S = self._S
670 X = self._X
671 Y = self._Y
672 for x, y in _zip(_2Floats(xs), _2Floats(ys, ys=True)): # strict=True
673 n1 = n
674 n += 1
675 if n1 > 0:
676 S += (X._M1 - x) * (Y._M1 - y) * (n1 / float(n))
677 X += x
678 Y += y
679 self._n = n
680 return _sampled(n, sample)
682 def fadd_(self, *x_ys, **sample):
683 '''Accumulate and return the current count.
685 @arg x_ys: Individual, alternating C{x, y, x, y, ...}
686 positional values (C{Scalar}s).
688 @see: Method C{Flinear.fadd}.
689 '''
690 return self.fadd(x_ys[0::2], x_ys[1::2], **sample)
692 def fcorrelation(self, sample=False):
693 '''Return the current, running (sample) correlation (C{float}).
695 @kwarg sample: Return the I{sample} instead of the entire
696 I{population} correlation (C{bool}).
697 '''
698 return self._sampled(self.x.fstdev(sample=sample) *
699 self.y.fstdev(sample=sample), sample)
701 def fintercept(self, sample=False):
702 '''Return the current, running (sample) intercept (C{float}).
704 @kwarg sample: Return the I{sample} instead of the entire
705 I{population} intercept (C{bool}).
706 '''
707 return float(self.y._M1 -
708 (self.x._M1 * self.fslope(sample=sample)))
710 def fslope(self, sample=False):
711 '''Return the current, running (sample) slope (C{float}).
713 @kwarg sample: Return the I{sample} instead of the entire
714 I{population} slope (C{bool}).
715 '''
716 return self._sampled(self.x.fvariance(sample=sample), sample)
718 def _sampled(self, t, sample):
719 '''(INTERNAL) Compute the sampled or entire population result.
720 '''
721 t *= float(_sampled(self._n, sample))
722 return float(self._S / t) if t else _0_0
724 @property_RO
725 def x(self):
726 '''Get the C{x} accumulator (L{Fcook} or L{Fwelford}).
727 '''
728 return self._X
730 @property_RO
731 def y(self):
732 '''Get the C{y} accumulator (L{Fcook} or L{Fwelford}).
733 '''
734 return self._Y
737__all__ += _ALL_DOCS(_FstatsBase, _FstatsNamed)
739# **) MIT License
740#
741# Copyright (C) 2021-2024 -- mrJean1 at Gmail -- All Rights Reserved.
742#
743# Permission is hereby granted, free of charge, to any person obtaining a
744# copy of this software and associated documentation files (the "Software"),
745# to deal in the Software without restriction, including without limitation
746# the rights to use, copy, modify, merge, publish, distribute, sublicense,
747# and/or sell copies of the Software, and to permit persons to whom the
748# Software is furnished to do so, subject to the following conditions:
749#
750# The above copyright notice and this permission notice shall be included
751# in all copies or substantial portions of the Software.
752#
753# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
754# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
755# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
756# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
757# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
758# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
759# OTHER DEALINGS IN THE SOFTWARE.