Coverage for pygeodesy/fstats.py: 99%
307 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-10-04 14:05 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-10-04 14:05 -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, Fmt
16from pygeodesy.interns import NN, _iadd_op_, _invalid_, _other_, _SPACE_
17from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY
18from pygeodesy.named import _Named, _NotImplemented, notOverloaded, \
19 property_RO
20# from pygeodesy.props import property_RO # from .named
21# from pygeodesy.streprs import Fmt # from .fsums
23# from math import sqrt # from .fmath
25__all__ = _ALL_LAZY.fstats
26__version__ = '23.09.22'
28_Floats = Fsum, float
29_Scalar = _Floats + (int,) # XXX basics._Ints is ABCMeta in Python 2
30try:
31 _Scalar += (long,)
32except NameError: # Python 3+
33 pass
36def _2Floats(xs, ys=False):
37 '''(INTERNAL) Yield each value as C{float} or L{Fsum}.
38 '''
39 if ys:
40 def _2f(i, x):
41 return _2float(index=i, ys=x)
42 else:
43 def _2f(i, x): # PYCHOK redef
44 return _2float(index=i, xs=x)
46 for i, x in enumerate(xs): # don't unravel Fsums
47 yield x if isinstance(x, _Floats) else _2f(i, x)
50def _sampled(n, sample):
51 '''(INTERNAL) Return the sample or the entire count.
52 '''
53 return (n - 1) if sample and n > 0 else n
56class _FstatsNamed(_Named):
57 '''(INTERNAL) Base class.
58 '''
59 _n = 0
61 def __add__(self, other):
62 '''Sum of this and a scalar, an L{Fsum} or an other instance.
63 '''
64 f = self.fcopy(name=self.__add__.__name__) # PYCHOK expected
65 f += other
66 return f
68 def __float__(self): # PYCHOK no cover
69 '''Not implemented.'''
70 return _NotImplemented(self)
72 def __int__(self): # PYCHOK no cover
73 '''Not implemented.'''
74 return _NotImplemented(self)
76 def __len__(self):
77 '''Return the I{total} number of accumulated values (C{int}).
78 '''
79 return self._n
81 def __neg__(self): # PYCHOK no cover
82 '''Not implemented.'''
83 return _NotImplemented(self)
85 def __radd__(self, other): # PYCHOK no cover
86 '''Not implemented.'''
87 return _NotImplemented(self, other)
89 def __str__(self):
90 return Fmt.SQUARE(self.named3, len(self))
92 def fcopy(self, deep=False, name=NN):
93 '''Copy this instance, C{shallow} or B{C{deep}}.
94 '''
95 n = name or self.fcopy.__name__
96 f = _Named.copy(self, deep=deep, name=n)
97 return self._copy(f, self) # PYCHOK expected
99 copy = fcopy
102class _FstatsBase(_FstatsNamed):
103 '''(INTERNAL) Base running stats class.
104 '''
105 _Ms = ()
107 def _copy(self, c, s):
108 '''(INTERNAL) Copy C{B{c} = B{s}}.
109 '''
110 _xinstanceof(self.__class__, c=c, s=s)
111 c._Ms = tuple(M.fcopy() for M in s._Ms) # deep=False
112 c._n = s._n
113 return c
115 def fadd(self, xs, sample=False): # PYCHOK no cover
116 '''I{Must be overloaded}.'''
117 notOverloaded(self, xs, sample=sample)
119 def fadd_(self, *xs, **sample):
120 '''Accumulate and return the current count.
122 @see: Method C{fadd}.
123 '''
124 return self.fadd(xs, **sample)
126 def fmean(self, xs=None):
127 '''Accumulate and return the current mean.
129 @kwarg xs: Iterable with additional values (C{Scalar}s).
131 @return: Current, running mean (C{float}).
133 @see: Method C{fadd}.
134 '''
135 if xs:
136 self.fadd(xs)
137 return self._M1.fsum()
139 def fmean_(self, *xs):
140 '''Accumulate and return the current mean.
142 @see: Method C{fmean}.
143 '''
144 return self.fmean(xs)
146 def fstdev(self, xs=None, sample=False):
147 '''Accumulate and return the current standard deviation.
149 @kwarg xs: Iterable with additional values (C{Scalar}).
150 @kwarg sample: Return the I{sample} instead of the entire
151 I{population} standard deviation (C{bool}).
153 @return: Current, running (sample) standard deviation (C{float}).
155 @see: Method C{fadd}.
156 '''
157 v = self.fvariance(xs, sample=sample)
158 return sqrt(v) if v > 0 else _0_0
160 def fstdev_(self, *xs, **sample):
161 '''Accumulate and return the current standard deviation.
163 @see: Method C{fstdev}.
164 '''
165 return self.fstdev(xs, **sample)
167 def fvariance(self, xs=None, sample=False):
168 '''Accumulate and return the current variance.
170 @kwarg xs: Iterable with additional values (C{Scalar}s).
171 @kwarg sample: Return the I{sample} instead of the entire
172 I{population} variance (C{bool}).
174 @return: Current, running (sample) variance (C{float}).
176 @see: Method C{fadd}.
177 '''
178 n = self.fadd(xs, sample=sample)
179 return float(self._M2 / float(n)) if n > 0 else _0_0
181 def fvariance_(self, *xs, **sample):
182 '''Accumulate and return the current variance.
184 @see: Method C{fvariance}.
185 '''
186 return self.fvariance(xs, **sample)
188 def _iadd_other(self, other):
189 '''(INTERNAL) Add Scalar or Scalars.
190 '''
191 if isinstance(other, _Scalar):
192 self.fadd_(other)
193 else:
194 try:
195 if not islistuple(other):
196 raise TypeError(_SPACE_(_invalid_, _other_))
197 self.fadd(other)
198 except Exception as x:
199 raise _xError(x, _SPACE_(self, _iadd_op_, repr(other)))
201 @property_RO
202 def _M1(self):
203 '''(INTERNAL) get the 1st Moment accumulator.'''
204 return self._Ms[0]
206 @property_RO
207 def _M2(self):
208 '''(INTERNAL) get the 2nd Moment accumulator.'''
209 return self._Ms[1]
212class Fcook(_FstatsBase):
213 '''U{Cook<https://www.JohnDCook.com/blog/skewness_kurtosis>}'s
214 C{RunningStats} computing the running mean, median and
215 (sample) kurtosis, skewness, variance, standard deviation
216 and Jarque-Bera normality.
218 @see: L{Fwelford} and U{Higher-order statistics<https://
219 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}.
220 '''
221 def __init__(self, xs=None, name=NN):
222 '''New L{Fcook} stats accumulator.
224 @kwarg xs: Iterable with initial values (C{Scalar}s).
225 @kwarg name: Optional name (C{str}).
227 @see: Method L{Fcook.fadd}.
228 '''
229 self._Ms = tuple(Fsum() for _ in range(4)) # 1st, 2nd ... Moment
230 if name:
231 self.name = name
232 if xs:
233 self.fadd(xs)
235 def __iadd__(self, other):
236 '''Add B{C{other}} to this L{Fcook} instance.
238 @arg other: An L{Fcook} instance or C{Scalar}s, meaning
239 one or more C{scalar} or L{Fsum} instances.
241 @return: This instance, updated (L{Fcook}).
243 @raise TypeError: Invalid B{C{other}} type.
245 @raise ValueError: Invalid B{C{other}}.
247 @see: Method L{Fcook.fadd}.
248 '''
249 if isinstance(other, Fcook):
250 nb = len(other)
251 if nb > 0:
252 na = len(self)
253 if na > 0:
254 A1, A2, A3, A4 = self._Ms
255 B1, B2, B3, B4 = other._Ms
257 n = na + nb
258 n_ = float(n)
259 D = A1 - B1 # b1 - a1
260 Dn = D / n_
261 Dn2 = Dn**2 # d**2 / n**2
262 nab = na * nb
263 Dn3 = Dn2 * (D * nab)
265 na2 = na**2
266 nb2 = nb**2
267 A4 += B4
268 A4 += (B3 * na - (A3 * nb)) * (Dn * _4_0)
269 A4 += (B2 * na2 + (A2 * nb2)) * (Dn2 * _6_0)
270 A4 += (Dn * Dn3) * (na2 - nab + nb2) # d**4 / n**3
272 A3 += B3
273 A3 += (A2 * na - (B2 * nb)) * (Dn * _3_0)
274 A3 += Dn3 * (na - nb)
276 A2 += B2
277 A2 += Dn2 * (nab / n_)
279 B1n = B1 * nb # if other is self
280 A1 *= na
281 A1 += B1n
282 A1 *= 1 / n_ # /= chokes PyChecker
284# self._Ms = A1, A2, A3, A4
285 self._n = n
286 else:
287 self._copy(self, other)
288 else:
289 self._iadd_other(other)
290 return self
292 def fadd(self, xs, sample=False):
293 '''Accumulate and return the current count.
295 @arg xs: Iterable with additional values (C{Scalar}s,
296 meaning C{scalar} or L{Fsum} instances).
297 @kwarg sample: Return the I{sample} instead of the entire
298 I{population} count (C{bool}).
300 @return: Current, running (sample) count (C{int}).
302 @raise OverflowError: Partial C{2sum} overflow.
304 @raise TypeError: Non-scalar B{C{xs}} value.
306 @raise ValueError: Invalid or non-finite B{C{xs}} value.
308 @see: U{online_kurtosis<https://WikiPedia.org/wiki/
309 Algorithms_for_calculating_variance>}.
310 '''
311 n = self._n
312 if xs:
313 M1, M2, M3, M4 = self._Ms
314 for x in _2Floats(xs):
315 n1 = n
316 n += 1
317 D = x - M1
318 Dn = D / n
319 if Dn:
320 Dn2 = Dn**2
321 if n1 > 1:
322 T1 = D * (Dn * n1)
323 T2 = T1 * (Dn * (n1 - 1))
324 T3 = T1 * (Dn2 * (n**2 - 3 * n1))
325 elif n1 > 0: # n1 == 1, n == 2
326 T1 = D * Dn
327 T2 = _0_0
328 T3 = T1 * Dn2
329 else:
330 T1 = T2 = T3 = _0_0
331 M4 += T3
332 M4 -= M3 * (Dn * _4_0)
333 M4 += M2 * (Dn2 * _6_0)
335 M3 += T2
336 M3 -= M2 * (Dn * _3_0)
338 M2 += T1
339 M1 += Dn
340# self._Ms = M1, M2, M3, M4
341 self._n = n
342 return _sampled(n, sample)
344 def fjb(self, xs=None, sample=True, excess=True):
345 '''Accumulate and compute the current U{Jarque-Bera
346 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality.
348 @kwarg xs: Iterable with additional values (C{Scalar}s).
349 @kwarg sample: Return the I{sample} normality (C{bool}), default.
350 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default.
352 @return: Current, running (sample) Jarque-Bera normality (C{float}).
354 @see: Method L{Fcook.fadd}.
355 '''
356 n = self.fadd(xs, sample=sample)
357 k = self.fkurtosis(sample=sample, excess=excess) / _2_0
358 s = self.fskewness(sample=sample)
359 return n * hypot2(k, s) / _6_0
361 def fjb_(self, *xs, **sample_excess):
362 '''Accumulate and compute the current U{Jarque-Bera
363 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality.
365 @see: Method L{Fcook.fjb}.
366 '''
367 return self.fjb(xs, **sample_excess)
369 def fkurtosis(self, xs=None, sample=False, excess=True):
370 '''Accumulate and return the current kurtosis.
372 @kwarg xs: Iterable with additional values (C{Scalar}s).
373 @kwarg sample: Return the I{sample} instead of the entire
374 I{population} kurtosis (C{bool}).
375 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default.
377 @return: Current, running (sample) kurtosis or I{excess} kurtosis (C{float}).
379 @see: U{Kurtosis Formula<https://www.Macroption.com/kurtosis-formula>}
380 and U{Mantalos<https://www.ResearchGate.net/publication/227440210>}.
382 @see: Method L{Fcook.fadd}.
383 '''
384 k, n = _0_0, self.fadd(xs, sample=sample)
385 if n > 0:
386 _, M2, _, M4 = self._Ms
387 m2 = float(M2 * M2)
388 if m2:
389 K, x = (M4 * (n / m2)), _3_0
390 if sample and 2 < n < len(self):
391 d = float((n - 1) * (n - 2))
392 K *= (n + 1) * (n + 2) / d
393 x *= n**2 / d
394 if excess:
395 K -= x
396 k = K.fsum()
397 return k
399 def fkurtosis_(self, *xs, **sample_excess):
400 '''Accumulate and return the current kurtosis.
402 @see: Method L{Fcook.fkurtosis}.
403 '''
404 return self.fkurtosis(xs, **sample_excess)
406 def fmedian(self, xs=None):
407 '''Accumulate and return the current median.
409 @kwarg xs: Iterable with additional values (C{Scalar}s).
411 @return: Current, running median (C{float}).
413 @see: U{Pearson's Skewness Coefficients<https://MathWorld.Wolfram.com/
414 PearsonsSkewnessCoefficients.html>}, U{Skewness & Kurtosis Simplified
415 https://TowardsDataScience.com/skewness-kurtosis-simplified-1338e094fc85>}
416 and method L{Fcook.fadd}.
417 '''
418 # skewness = 3 * (mean - median) / stdev, i.e.
419 # median = mean - skewness * stdef / 3
420 m = float(self._M1) if xs is None else self.fmean(xs)
421 return m - self.fskewness() * self.fstdev() / _3_0
423 def fmedian_(self, *xs):
424 '''Accumulate and return the current median.
426 @see: Method L{Fcook.fmedian}.
427 '''
428 return self.fmedian(xs)
430 def fskewness(self, xs=None, sample=False):
431 '''Accumulate and return the current skewness.
433 @kwarg xs: Iterable with additional values (C{Scalar}s).
434 @kwarg sample: Return the I{sample} instead of the entire
435 I{population} skewness (C{bool}).
437 @return: Current, running (sample) skewness (C{float}).
439 @see: U{Skewness Formula<https://www.Macroption.com/skewness-formula/>}
440 and U{Mantalos<https://www.ResearchGate.net/publication/227440210>}.
442 @see: Method L{Fcook.fadd}.
443 '''
444 s, n = _0_0, self.fadd(xs, sample=sample)
445 if n > 0:
446 _, M2, M3, _ = self._Ms
447 m = float(M2**3)
448 if m > 0:
449 S = M3 * sqrt(float(n) / m)
450 if sample and 1 < n < len(self):
451 S *= (n + 1) / float(n - 1)
452 s = S.fsum()
453 return s
455 def fskewness_(self, *xs, **sample):
456 '''Accumulate and return the current skewness.
458 @see: Method L{Fcook.fskewness}.
459 '''
460 return self.fskewness(xs, **sample)
462 def toFwelford(self, name=NN):
463 '''Return an L{Fwelford} equivalent.
464 '''
465 f = Fwelford(name=name or self.name)
466 f._Ms = self._M1.fcopy(), self._M2.fcopy() # deep=False
467 f._n = self._n
468 return f
471class Fwelford(_FstatsBase):
472 '''U{Welford<https://WikiPedia.org/wiki/Algorithms_for_calculating_variance>}'s
473 accumulator computing the running mean, (sample) variance and standard deviation.
475 @see: U{Cook<https://www.JohnDCook.com/blog/standard_deviation/>} and L{Fcook}.
476 '''
477 def __init__(self, xs=None, name=NN):
478 '''New L{Fwelford} stats accumulator.
480 @kwarg xs: Iterable with initial values (C{Scalar}s).
481 @kwarg name: Optional name (C{str}).
483 @see: Method L{Fwelford.fadd}.
484 '''
485 self._Ms = Fsum(), Fsum() # 1st and 2nd Moment
486 if name:
487 self.name = name
488 if xs:
489 self.fadd(xs)
491 def __iadd__(self, other):
492 '''Add B{C{other}} to this L{Fwelford} instance.
494 @arg other: An L{Fwelford} or L{Fcook} instance or C{Scalar}s,
495 meaning one or more C{scalar} or L{Fsum} instances.
497 @return: This instance, updated (L{Fwelford}).
499 @raise TypeError: Invalid B{C{other}} type.
501 @raise ValueError: Invalid B{C{other}}.
503 @see: Method L{Fwelford.fadd} and U{Parallel algorithm<https//
504 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}.
505 '''
506 if isinstance(other, Fwelford):
507 nb = len(other)
508 if nb > 0:
509 na = len(self)
510 if na > 0:
511 M, S = self._Ms
512 M_, S_ = other._Ms
514 n = na + nb
515 n_ = float(n)
517 D = M_ - M
518 D *= D # D**2
519 D *= na * nb / n_
520 S += D
521 S += S_
523 Mn = M_ * nb # if other is self
524 M *= na
525 M += Mn
526 M *= 1 / n_ # /= chokes PyChecker
528# self._Ms = M, S
529 self._n = n
530 else:
531 self._copy(self, other)
533 elif isinstance(other, Fcook):
534 self += other.toFwelford()
535 else:
536 self._iadd_other(other)
537 return self
539 def fadd(self, xs, sample=False):
540 '''Accumulate and return the current count.
542 @arg xs: Iterable with additional values (C{Scalar}s,
543 meaning C{scalar} or L{Fsum} instances).
544 @kwarg sample: Return the I{sample} instead of the entire
545 I{population} count (C{bool}).
547 @return: Current, running (sample) count (C{int}).
549 @raise OverflowError: Partial C{2sum} overflow.
551 @raise TypeError: Non-scalar B{C{xs}} value.
553 @raise ValueError: Invalid or non-finite B{C{xs}} value.
554 '''
555 n = self._n
556 if xs:
557 M, S = self._Ms
558 for x in _2Floats(xs):
559 n += 1
560 D = x - M
561 M += D / n
562 D *= x - M
563 S += D
564# self._Ms = M, S
565 self._n = n
566 return _sampled(n, sample)
569class Flinear(_FstatsNamed):
570 '''U{Cook<https://www.JohnDCook.com/blog/running_regression>}'s
571 C{RunningRegression} computing the running slope, intercept
572 and correlation of a linear regression.
573 '''
574 def __init__(self, xs=None, ys=None, Fstats=Fwelford, name=NN):
575 '''New L{Flinear} regression accumulator.
577 @kwarg xs: Iterable with initial C{x} values (C{Scalar}s).
578 @kwarg ys: Iterable with initial C{y} values (C{Scalar}s).
579 @kwarg Fstats: Stats class for C{x} and C{y} values (L{Fcook}
580 or L{Fwelford}).
581 @kwarg name: Optional name (C{str}).
583 @raise TypeError: Invalid B{C{Fs}}, not L{Fcook} or
584 L{Fwelford}.
585 @see: Method L{Flinear.fadd}.
586 '''
587 _xsubclassof(Fcook, Fwelford, Fstats=Fstats)
588 if name:
589 self.name = name
591 self._S = Fsum(name=name)
592 self._X = Fstats(name=name)
593 self._Y = Fstats(name=name)
594 if xs and ys:
595 self.fadd(xs, ys)
597 def __iadd__(self, other):
598 '''Add B{C{other}} to this instance.
600 @arg other: An L{Flinear} instance or C{Scalar} pairs,
601 meaning C{scalar} or L{Fsum} instances.
603 @return: This instance, updated (L{Flinear}).
605 @raise TypeError: Invalid B{C{other}} or the B{C{other}}
606 and these C{x} and C{y} accumulators
607 are not compatible.
609 @raise ValueError: Invalid or odd-length B{C{other}}.
611 @see: Method L{Flinear.fadd_}.
612 '''
613 if isinstance(other, Flinear):
614 if len(other) > 0:
615 if len(self) > 0:
616 n = other._n
617 S = other._S
618 X = other._X
619 Y = other._Y
620 D = (X._M1 - self._X._M1) * \
621 (Y._M1 - self._Y._M1) * \
622 (n * self._n / float(n + self._n))
623 self._n += n
624 self._S += S + D
625 self._X += X
626 self._Y += Y
627 else:
628 self._copy(self, other)
629 else:
630 try:
631 if not islistuple(other):
632 raise TypeError(_SPACE_(_invalid_, _other_))
633 elif isodd(len(other)):
634 raise ValueError(Fmt.PAREN(isodd=Fmt.PAREN(len=_other_)))
635 self.fadd_(*other)
636 except Exception as x:
637 raise _xError(x, _SPACE_(self, _iadd_op_, repr(other)))
638 return self
640 def _copy(self, c, s):
641 '''(INTERNAL) Copy C{B{c} = B{s}}.
642 '''
643 _xinstanceof(Flinear, c=c, s=s)
644 c._n = s._n
645 c._S = s._S.fcopy(deep=False)
646 c._X = s._X.fcopy(deep=False)
647 c._Y = s._Y.fcopy(deep=False)
648 return c
650 def fadd(self, xs, ys, sample=False):
651 '''Accumulate and return the current count.
653 @arg xs: Iterable with additional C{x} values (C{Scalar}s),
654 meaning C{scalar} or L{Fsum} instances).
655 @arg ys: Iterable with additional C{y} values (C{Scalar}s,
656 meaning C{scalar} or L{Fsum} instances).
657 @kwarg sample: Return the I{sample} instead of the entire
658 I{population} count (C{bool}).
660 @return: Current, running (sample) count (C{int}).
662 @raise OverflowError: Partial C{2sum} overflow.
664 @raise TypeError: Non-scalar B{C{xs}} or B{C{ys}} value.
666 @raise ValueError: Invalid or non-finite B{C{xs}} or B{C{ys}} value.
667 '''
668 n = self._n
669 if xs and ys:
670 S = self._S
671 X = self._X
672 Y = self._Y
673 for x, y in _zip(_2Floats(xs), _2Floats(ys, ys=True)): # strict=True
674 n1 = n
675 n += 1
676 if n1 > 0:
677 S += (X._M1 - x) * (Y._M1 - y) * (n1 / float(n))
678 X += x
679 Y += y
680 self._n = n
681 return _sampled(n, sample)
683 def fadd_(self, *x_ys, **sample):
684 '''Accumulate and return the current count.
686 @arg x_ys: Individual, alternating C{x, y, x, y, ...}
687 positional values (C{Scalar}s).
689 @see: Method C{Flinear.fadd}.
690 '''
691 return self.fadd(x_ys[0::2], x_ys[1::2], **sample)
693 def fcorrelation(self, sample=False):
694 '''Return the current, running (sample) correlation (C{float}).
696 @kwarg sample: Return the I{sample} instead of the entire
697 I{population} correlation (C{bool}).
698 '''
699 return self._sampled(self.x.fstdev(sample=sample) *
700 self.y.fstdev(sample=sample), sample)
702 def fintercept(self, sample=False):
703 '''Return the current, running (sample) intercept (C{float}).
705 @kwarg sample: Return the I{sample} instead of the entire
706 I{population} intercept (C{bool}).
707 '''
708 return float(self.y._M1 -
709 (self.x._M1 * self.fslope(sample=sample)))
711 def fslope(self, sample=False):
712 '''Return the current, running (sample) slope (C{float}).
714 @kwarg sample: Return the I{sample} instead of the entire
715 I{population} slope (C{bool}).
716 '''
717 return self._sampled(self.x.fvariance(sample=sample), sample)
719 def _sampled(self, t, sample):
720 '''(INTERNAL) Compute the sampled or entire population result.
721 '''
722 t *= float(_sampled(self._n, sample))
723 return float(self._S / t) if t else _0_0
725 @property_RO
726 def x(self):
727 '''Get the C{x} accumulator (L{Fcook} or L{Fwelford}).
728 '''
729 return self._X
731 @property_RO
732 def y(self):
733 '''Get the C{y} accumulator (L{Fcook} or L{Fwelford}).
734 '''
735 return self._Y
738__all__ += _ALL_DOCS(_FstatsBase, _FstatsNamed)
740# **) MIT License
741#
742# Copyright (C) 2021-2023 -- mrJean1 at Gmail -- All Rights Reserved.
743#
744# Permission is hereby granted, free of charge, to any person obtaining a
745# copy of this software and associated documentation files (the "Software"),
746# to deal in the Software without restriction, including without limitation
747# the rights to use, copy, modify, merge, publish, distribute, sublicense,
748# and/or sell copies of the Software, and to permit persons to whom the
749# Software is furnished to do so, subject to the following conditions:
750#
751# The above copyright notice and this permission notice shall be included
752# in all copies or substantial portions of the Software.
753#
754# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
755# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
756# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
757# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
758# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
759# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
760# OTHER DEALINGS IN THE SOFTWARE.