Coverage for pygeodesy/fstats.py: 98%

325 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Classes for I{running} statistics and regressions based on 

5L{pygeodesy.Fsum}, precision floating point summation and accurate 

6multiplication. 

7''' 

8# make sure int/int division yields float quotient, see .basics 

9from __future__ import division as _; del _ # PYCHOK semicolon 

10 

11from pygeodesy.basics import isscalar, isodd, _xinstanceof, \ 

12 _xiterable, _xsubclassof, _zip, typename 

13from pygeodesy.constants import _0_0, _1_0, _2_0, _3_0, _4_0, _6_0 

14from pygeodesy.errors import _ValueError, _xError, _xkwds_item2, \ 

15 _xsError 

16from pygeodesy.fmath import Fsqrt, Fmt 

17from pygeodesy.fsums import _2finite, Fsum, _iadd_op_, _isFsum_2Tuple 

18# from pygeodesy.internals import typename # from .basics 

19from pygeodesy.interns import _odd_, _SPACE_ 

20from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY 

21from pygeodesy.named import _name__, _Named, _NotImplemented, \ 

22 property_RO 

23# from pygeodesy.props import property_RO # from .named 

24# from pygeodesy.streprs import Fmt # from .fmath 

25 

26__all__ = _ALL_LAZY.fstats 

27__version__ = '25.04.14' 

28 

29 

30def _sampled(n, sample): 

31 '''(INTERNAL) Return the sample or the entire count. 

32 ''' 

33 return (n - 1) if sample and n > 0 else n 

34 

35 

36def _Xs(**name_xs): 

37 '''(INTERNAL) Yield all C{xs} as C{float} or L{Fsum}. 

38 ''' 

39 name, xs = _xkwds_item2(name_xs) 

40 try: 

41 i, x = 0, xs 

42 for i, x in enumerate(xs): # don't unravel Fsums 

43 yield x._Fsum if _isFsum_2Tuple(x) else x 

44 except Exception as X: 

45 raise _xsError(X, xs, i, x, name) 

46 

47 

48class _FstatsNamed(_Named): 

49 '''(INTERNAL) Base class. 

50 ''' 

51 _n = 0 

52 

53 def __add__(self, other): 

54 '''Sum of this and an other instance, a C{scalar}, an L{Fsum} 

55 or L{Fsum2Tuple}. 

56 ''' 

57 f = self.copy(name__=self.__add__) # PYCHOK expected 

58 f += other 

59 return f 

60 

61 def __float__(self): # PYCHOK no cover 

62 '''Not implemented.''' 

63 return _NotImplemented(self) 

64 

65 def __int__(self): # PYCHOK no cover 

66 '''Not implemented.''' 

67 return _NotImplemented(self) 

68 

69 def __len__(self): 

70 '''Return the I{total} number of accumulated C{Scalars} (C{int}). 

71 ''' 

72 return self._n 

73 

74 def __neg__(self): # PYCHOK no cover 

75 '''Not implemented.''' 

76 return _NotImplemented(self) 

77 

78 def __radd__(self, other): # PYCHOK no cover 

79 '''Not implemented.''' 

80 return _NotImplemented(self, other) 

81 

82 def __str__(self): 

83 n = self.name 

84 n = _SPACE_(self.classname, n) if n else self.classname 

85 return Fmt.SQUARE(n, len(self)) 

86 

87 def copy(self, deep=False, **name): 

88 '''Copy this instance, C{shallow} or B{C{deep}}. 

89 

90 @kwarg name: Optional, overriding C{B{name}="copy"} (C{str}). 

91 

92 @return: The copy instance. 

93 ''' 

94 n = _name__(name, name__=self.copy) 

95 f = _Named.copy(self, deep=deep, name=n) 

96 return self._copy(f, self) # PYCHOK expected 

97 

98 fcopy = copy # for backward compatibility 

99 

100 

101class _FstatsBase(_FstatsNamed): 

102 '''(INTERNAL) Base running stats class. 

103 ''' 

104 _Ms = () 

105 

106 def _copy(self, d, s): 

107 '''(INTERNAL) Copy C{B{d} = B{s}}. 

108 ''' 

109 _xinstanceof(self.__class__, d=d, s=s) 

110 d._Ms = tuple(M.copy() for M in s._Ms) # deep=False 

111 d._n = s._n 

112 return d 

113 

114 def fadd(self, xs, sample=False): # PYCHOK no cover 

115 '''I{Must be overloaded}.''' 

116 self._notOverloaded(xs, sample=sample) 

117 

118 def fadd_(self, *xs, **sample): 

119 '''Accumulate and return the current count. 

120 

121 @see: Method C{fadd} for further details. 

122 ''' 

123 return self.fadd(xs, **sample) 

124 

125 def fmean(self, xs=None): 

126 '''Accumulate and return the current mean. 

127 

128 @kwarg xs: Iterable of additional values (each C{scalar}, 

129 an L{Fsum} or L{Fsum2Tuple}). 

130 

131 @return: Current, running mean (C{float}). 

132 

133 @see: Method C{fadd}. 

134 ''' 

135 return float(self._Mean(xs)) 

136 

137 def fmean_(self, *xs): 

138 '''Accumulate and return the current mean. 

139 

140 @see: Method C{fmean} for further details. 

141 ''' 

142 return self.fmean(xs) 

143 

144 def fstdev(self, xs=None, **sample): 

145 '''Accumulate and return the current standard deviation. 

146 

147 @arg xs: Iterable of additional values (each C{scalar}, an L{Fsum} 

148 or L{Fsum2Tuple}). 

149 @kwarg sample: Use C{B{sample}=True} for the I{sample} deviation 

150 instead of the I{population} deviation (C{bool}). 

151 

152 @return: Current, running (sample) standard deviation (C{float}). 

153 

154 @see: Method C{fadd}. 

155 ''' 

156 return float(self._Stdev(xs, **sample)) 

157 

158 def fstdev_(self, *xs, **sample): 

159 '''Accumulate and return the current standard deviation. 

160 

161 @see: Method C{fstdev} for further details. 

162 ''' 

163 return self.fstdev(xs, **sample) 

164 

165 def fvariance(self, xs=None, **sample): 

166 '''Accumulate and return the current variance. 

167 

168 @arg xs: Iterable of additional values (each C{scalar}, an L{Fsum} 

169 or L{Fsum2Tuple}). 

170 @kwarg sample: Use C{B{sample}=True} for the I{sample} variance 

171 instead of the I{population} variance (C{bool}). 

172 

173 @return: Current, running (sample) variance (C{float}). 

174 

175 @see: Method C{fadd}. 

176 ''' 

177 return float(self._Variance(xs, **sample)) 

178 

179 def fvariance_(self, *xs, **sample): 

180 '''Accumulate and return the current variance. 

181 

182 @see: Method C{fvariance} for further details. 

183 ''' 

184 return self.fvariance(xs, **sample) 

185 

186 def _iadd_other(self, other): 

187 '''(INTERNAL) Add one or several values. 

188 ''' 

189 try: 

190 if _isFsum_2Tuple(other): 

191 self.fadd_(other._Fsum) 

192 elif isscalar(other): 

193 self.fadd_(_2finite(other)) 

194 elif _xiterable(other): 

195 self.fadd(other) 

196 except Exception as X: 

197 t = _SPACE_(self, _iadd_op_, repr(other)) 

198 raise _xError(X, t) 

199 

200 @property_RO 

201 def _M1(self): 

202 '''(INTERNAL) get the 1st Moment accumulator.''' 

203 return self._Ms[0] 

204 

205 @property_RO 

206 def _M2(self): 

207 '''(INTERNAL) get the 2nd Moment accumulator.''' 

208 return self._Ms[1] 

209 

210 def _Mean(self, xs=None): 

211 '''(INTERNAL) Return the current mean as L{Fsum}. 

212 ''' 

213 if xs: 

214 self.fadd(xs) 

215 return self._M1 # .copy() 

216 

217 def _Stdev(self, xs=None, **sample): 

218 '''(INTERNAL) Return the current (sample) standard deviation as L{Fsum}. 

219 ''' 

220 V = self._Variance(xs, **sample) 

221 return Fsqrt(V) if V > 0 else _0_0 

222 

223 def _Variance(self, xs=None, **sample): 

224 '''(INTERNAL) Return the current (sample) variance as L{Fsum}. 

225 ''' 

226 n = self.fadd(xs, **sample) 

227 return (self._M2 / n) if n > 0 else _0_0 

228 

229 

230class Fcook(_FstatsBase): 

231 '''U{Cook<https://www.JohnDCook.com/blog/skewness_kurtosis>}'s 

232 C{RunningStats} computing the running mean, median and 

233 (sample) kurtosis, skewness, variance, standard deviation 

234 and Jarque-Bera normality. 

235 

236 @see: L{Fwelford} and U{Higher-order statistics<https:// 

237 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}. 

238 ''' 

239 def __init__(self, xs=None, **name): 

240 '''New L{Fcook} stats accumulator. 

241 

242 @arg xs: Iterable of additional values (each C{scalar}, an 

243 L{Fsum} or L{Fsum2Tuple}). 

244 @kwarg name: Optional C{B{name}=NN} (C{str}). 

245 

246 @see: Method L{Fcook.fadd}. 

247 ''' 

248 self._Ms = tuple(Fsum() for _ in range(4)) # 1st, 2nd ... Moment 

249 if name: 

250 self.name = name 

251 if xs: 

252 self.fadd(xs) 

253 

254 def __iadd__(self, other): 

255 '''Add B{C{other}} to this L{Fcook} instance. 

256 

257 @arg other: An L{Fcook} instance or value or iterable 

258 of values (each C{scalar}, an L{Fsum} or 

259 L{Fsum2Tuple}). 

260 

261 @return: This instance, updated (L{Fcook}). 

262 

263 @raise TypeError: Invalid B{C{other}}. 

264 

265 @raise ValueError: Invalid or non-finite B{C{other}}. 

266 

267 @see: Method L{Fcook.fadd}. 

268 ''' 

269 if isinstance(other, Fcook): 

270 nb = len(other) 

271 if nb > 0: 

272 na = len(self) 

273 if na > 0: 

274 A1, A2, A3, A4 = self._Ms 

275 B1, B2, B3, B4 = other._Ms 

276 

277 n = na + nb 

278 _n = _1_0 / n 

279 D = A1 - B1 # b1 - a1 

280 Dn = D * _n 

281 Dn2 = Dn**2 # d**2 / n**2 

282 nab = na * nb 

283 Dn3 = Dn2 * (D * nab) 

284 

285 na2 = na**2 

286 nb2 = nb**2 

287 A4 += B4 

288 A4 += (B3 * na - (A3 * nb)) * (Dn * _4_0) 

289 A4 += (B2 * na2 + (A2 * nb2)) * (Dn2 * _6_0) 

290 A4 += (Dn * Dn3) * (na2 - nab + nb2) # d**4 / n**3 

291 

292 A3 += B3 

293 A3 += (A2 * na - (B2 * nb)) * (Dn * _3_0) 

294 A3 += Dn3 * (na - nb) 

295 

296 A2 += B2 

297 A2 += Dn2 * (nab * _n) 

298 

299 B1n = B1 * nb # if other is self 

300 A1 *= na 

301 A1 += B1n 

302 A1 *= _n 

303 

304# self._Ms = A1, A2, A3, A4 

305 self._n = n 

306 else: 

307 self._copy(self, other) 

308 else: 

309 self._iadd_other(other) 

310 return self 

311 

312 def fadd(self, xs, sample=False): 

313 '''Accumulate and return the current count. 

314 

315 @arg xs: Iterable of additional values (each C{scalar}, an 

316 L{Fsum} or L{Fsum2Tuple}). 

317 @kwarg sample: Use C{B{sample}=True} for the I{sample} count 

318 instead of the I{population} count (C{bool}). 

319 

320 @return: Current, running (sample) count (C{int}). 

321 

322 @raise OverflowError: Partial C{2sum} overflow. 

323 

324 @raise TypeError: Invalid B{C{xs}}. 

325 

326 @raise ValueError: Invalid or non-finite B{C{xs}}. 

327 

328 @see: U{online_kurtosis<https://WikiPedia.org/wiki/ 

329 Algorithms_for_calculating_variance>}. 

330 ''' 

331 n = self._n 

332 if xs: 

333 M1, M2, M3, M4 = self._Ms 

334 for x in _Xs(xs=xs): # PYCHOK yield 

335 n1 = n 

336 n += 1 

337 D = x - M1 

338 Dn = D / n 

339 if Dn: 

340 Dn2 = Dn**2 

341 if n1 > 1: 

342 T1 = D * (Dn * n1) 

343 T2 = T1 * (Dn * (n1 - 1)) 

344 T3 = T1 * (Dn2 * (n**2 - 3 * n1)) 

345 elif n1 > 0: # n1 == 1, n == 2 

346 T1 = D * Dn 

347 T2 = _0_0 

348 T3 = T1 * Dn2 

349 else: 

350 T1 = T2 = T3 = _0_0 

351 M4 += T3 

352 M4 -= M3 * (Dn * _4_0) 

353 M4 += M2 * (Dn2 * _6_0) 

354 

355 M3 += T2 

356 M3 -= M2 * (Dn * _3_0) 

357 

358 M2 += T1 

359 M1 += Dn 

360# self._Ms = M1, M2, M3, M4 

361 self._n = n 

362 return _sampled(n, sample) 

363 

364 def fjb(self, xs=None, excess=True, sample=True): 

365 '''Accumulate and compute the current U{Jarque-Bera 

366 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality. 

367 

368 @kwarg xs: Iterable of additional values (each C{scalar}, an 

369 L{Fsum} or L{Fsum2Tuple}). 

370 @kwarg excess: Apply the I{excess} kurtosis (C{bool}), default. 

371 @kwarg sample: Use C{B{sample}=False} for the I{population} 

372 normality instead of the I{sample} one (C{bool}). 

373 

374 @return: Current, running (sample) Jarque-Bera normality (C{float}). 

375 

376 @see: Method L{Fcook.fadd}. 

377 ''' 

378 return float(self._JarqueBera(xs, excess, sample=sample)) 

379 

380 def fjb_(self, *xs, **sample_excess): 

381 '''Accumulate and compute the current U{Jarque-Bera 

382 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality. 

383 

384 @see: Method L{Fcook.fjb} for further details. 

385 ''' 

386 return self.fjb(xs, **sample_excess) 

387 

388 def fkurtosis(self, xs=None, excess=True, **sample): 

389 '''Accumulate and return the current kurtosis. 

390 

391 @arg xs: Iterable of additional values (each C{scalar}, an L{Fsum} 

392 or L{Fsum2Tuple}). 

393 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default. 

394 @kwarg sample: Use C{B{sample}=True} for the I{sample} kurtosis 

395 instead of the I{population} kurtosis (C{bool}). 

396 

397 @return: Current, running (sample) kurtosis or I{excess} kurtosis (C{float}). 

398 

399 @see: U{Kurtosis Formula<https://www.Macroption.com/kurtosis-formula>} 

400 and U{Mantalos<https://www.ResearchGate.net/publication/227440210>}. 

401 

402 @see: Method L{Fcook.fadd}. 

403 ''' 

404 n = self.fadd(xs, **sample) 

405 return float(self._Kurtosis(n, excess, **sample)) 

406 

407 def fkurtosis_(self, *xs, **excess_sample): 

408 '''Accumulate and return the current kurtosis. 

409 

410 @see: Method L{Fcook.fkurtosis} for further details. 

411 ''' 

412 return self.fkurtosis(xs, **excess_sample) 

413 

414 def fmedian(self, xs=None): 

415 '''Accumulate and return the current median. 

416 

417 @arg xs: Iterable of additional values (each C{scalar}, an L{Fsum} or 

418 L{Fsum2Tuple}). 

419 

420 @return: Current, running median (C{float}). 

421 

422 @see: U{Pearson's Skewness Coefficients<https://MathWorld.Wolfram.com/ 

423 PearsonsSkewnessCoefficients.html>}, U{Skewness & Kurtosis Simplified 

424 https://TowardsDataScience.com/skewness-kurtosis-simplified-1338e094fc85>} 

425 and method L{Fcook.fadd}. 

426 ''' 

427 return float(self._Median(xs)) 

428 

429 def fmedian_(self, *xs): 

430 '''Accumulate and return the current median. 

431 

432 @see: Method L{Fcook.fmedian} for further details. 

433 ''' 

434 return self.fmedian(xs) 

435 

436 def fskewness(self, xs=None, **sample): 

437 '''Accumulate and return the current skewness. 

438 

439 @arg xs: Iterable of additional values (each C{scalar}, an L{Fsum} 

440 or L{Fsum2Tuple}). 

441 @kwarg sample: Use C{B{sample}=True} for the I{sample} skewness 

442 instead of the I{population} skewness (C{bool}). 

443 

444 @return: Current, running (sample) skewness (C{float}). 

445 

446 @see: U{Skewness Formula<https://www.Macroption.com/skewness-formula/>} 

447 and U{Mantalos<https://www.ResearchGate.net/publication/227440210>}. 

448 

449 @see: Method L{Fcook.fadd}. 

450 ''' 

451 n = self.fadd(xs, **sample) 

452 return float(self._Skewness(n, **sample)) 

453 

454 def fskewness_(self, *xs, **sample): 

455 '''Accumulate and return the current skewness. 

456 

457 @see: Method L{Fcook.fskewness} for further details. 

458 ''' 

459 return self.fskewness(xs, **sample) 

460 

461 def _JarqueBera(self, xs, excess, **sample): 

462 '''(INTERNAL) Return the (sample) Jarque-Bera normality as L{Fsum}. 

463 ''' 

464 N, n = _0_0, self.fadd(xs, **sample) 

465 if n > 0: 

466 K = self._Kurtosis(n, excess, **sample) / _2_0 

467 S = self._Skewness(n, **sample) 

468 N = (K**2 + S**2) * (n / _6_0) # Fpowers(2, K, S) * ... 

469 return N 

470 

471 def _Kurtosis(self, n, excess, sample=False): 

472 '''(INTERNAL) Return the (sample) kurtosis as L{Fsum} or C{0.0}. 

473 ''' 

474 K = _0_0 

475 if n > 0: 

476 _, M2, _, M4 = self._Ms 

477 M = M2**2 

478 if M > 0: 

479 K, x = M.rdiv(M4 * n, raiser=False), _3_0 

480 if sample and 2 < n < len(self): 

481 d = (n - 1) * (n - 2) 

482 K *= (n + 1) * (n + 2) / d 

483 x *= n**2 / d 

484 if excess: 

485 K -= x 

486 return K 

487 

488 def _Median(self, xs=None): 

489 '''(INTERNAL) Return the median as L{Fsum}. 

490 ''' 

491 # skewness = 3 * (mean - median) / stdev, i.e. 

492 # median = mean - (skewness * stdef) / 3 

493 return self._Mean(xs) - (self._Skewness(self._n) * 

494 self._Stdev()) / _3_0 

495 

496 def _Skewness(self, n, sample=False): 

497 '''(INTERNAL) Return the (sample) skewness as L{Fsum} or C{0.0}. 

498 ''' 

499 S = _0_0 

500 if n > 0: 

501 _, M2, M3, _ = self._Ms 

502 M = M2**3 

503 if M > 0: 

504 M = M.rdiv(n, raiser=False) 

505 S = M3 * Fsqrt(M, raiser=False) 

506 if sample and 1 < n < len(self): 

507 S *= (n + 1) / (n - 1) 

508 return S 

509 

510 def toFwelford(self, **name): 

511 '''Return a L{Fwelford} equivalent. 

512 

513 @kwarg name: Optional C{B{name}=NN} (C{str}). 

514 ''' 

515 f = Fwelford(name=self._name__(name)) 

516 f._Ms = self._M1.copy(), self._M2.copy() # deep=False 

517 f._n = self._n 

518 return f 

519 

520 

521class Fwelford(_FstatsBase): 

522 '''U{Welford<https://WikiPedia.org/wiki/Algorithms_for_calculating_variance>}'s 

523 accumulator computing the running mean, (sample) variance and standard deviation. 

524 

525 @see: U{Cook<https://www.JohnDCook.com/blog/standard_deviation/>} and L{Fcook}. 

526 ''' 

527 def __init__(self, xs=None, **name): 

528 '''New L{Fwelford} stats accumulator. 

529 

530 @arg xs: Iterable of initial values (each C{scalar}, an L{Fsum} or L{Fsum2Tuple}). 

531 @kwarg name: Optional C{B{name}=NN} (C{str}). 

532 

533 @see: Method L{Fwelford.fadd}. 

534 ''' 

535 self._Ms = Fsum(), Fsum() # 1st and 2nd Moment 

536 if name: 

537 self.name = name 

538 if xs: 

539 self.fadd(xs) 

540 

541 def __iadd__(self, other): 

542 '''Add B{C{other}} to this L{Fwelford} instance. 

543 

544 @arg other: An L{Fwelford} or L{Fcook} instance or value or an 

545 iterable of values (each C{scalar}, an L{Fsum} or 

546 L{Fsum2Tuple}). 

547 

548 @return: This instance, updated (L{Fwelford}). 

549 

550 @raise TypeError: Invalid B{C{other}}. 

551 

552 @raise ValueError: Invalid B{C{other}}. 

553 

554 @see: Method L{Fwelford.fadd} and U{Parallel algorithm<https// 

555 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}. 

556 ''' 

557 if isinstance(other, Fwelford): 

558 nb = len(other) 

559 if nb > 0: 

560 na = len(self) 

561 if na > 0: 

562 M, S = self._Ms 

563 M_, S_ = other._Ms 

564 

565 n = na + nb 

566 _n = _1_0 / n 

567 

568 D = M_ - M 

569 D *= D # D**2 

570 D *= na * nb * _n 

571 S += D 

572 S += S_ 

573 

574 Mn = M_ * nb # if other is self 

575 M *= na 

576 M += Mn 

577 M *= _n 

578 

579# self._Ms = M, S 

580 self._n = n 

581 else: 

582 self._copy(self, other) 

583 

584 elif isinstance(other, Fcook): 

585 self += other.toFwelford() 

586 else: 

587 self._iadd_other(other) 

588 return self 

589 

590 def fadd(self, xs, sample=False): 

591 '''Accumulate and return the current count. 

592 

593 @arg xs: Iterable of additional values (each C{scalar}, an 

594 L{Fsum} or L{Fsum2Tuple}). 

595 @kwarg sample: Use C{B{sample}=True} for the I{sample} count 

596 instead of the I{population} count (C{bool}). 

597 

598 @return: Current, running (sample) count (C{int}). 

599 

600 @raise OverflowError: Partial C{2sum} overflow. 

601 

602 @raise TypeError: Invalid B{C{xs}}. 

603 

604 @raise ValueError: Invalid or non-finite B{C{xs}}. 

605 ''' 

606 n = self._n 

607 if xs: 

608 M, S = self._Ms 

609 for x in _Xs(xs=xs): # PYCHOK yield 

610 n += 1 

611 D = x - M 

612 M += D / n 

613 D *= x - M 

614 S += D 

615# self._Ms = M, S 

616 self._n = n 

617 return _sampled(n, sample) 

618 

619 

620class Flinear(_FstatsNamed): 

621 '''U{Cook<https://www.JohnDCook.com/blog/running_regression>}'s 

622 C{RunningRegression} computing the running slope, intercept 

623 and correlation of a linear regression. 

624 ''' 

625 def __init__(self, xs=None, ys=None, Fstats=Fwelford, **name): 

626 '''New L{Flinear} regression accumulator. 

627 

628 @kwarg xs: Iterable of initial C{x} values (each C{scalar}, 

629 an L{Fsum} or L{Fsum2Tuple}). 

630 @kwarg ys: Iterable of initial C{y} values (each C{scalar}, 

631 an L{Fsum} or L{Fsum2Tuple}). 

632 @kwarg Fstats: Class for C{xs} and C{ys} values (L{Fcook} or 

633 L{Fwelford}). 

634 @kwarg name: Optional C{B{name}=NN} (C{str}). 

635 

636 @raise TypeError: B{C{Fstats}} not L{Fcook} or L{Fwelford}. 

637 

638 @see: Method L{Flinear.fadd}. 

639 ''' 

640 _xsubclassof(Fcook, Fwelford, Fstats=Fstats) 

641 if name: 

642 self.name = name 

643 

644 self._S = Fsum(name=name) 

645 self._X = Fstats(name=name) 

646 self._Y = Fstats(name=name) 

647 if xs and ys: 

648 self.fadd(xs, ys) 

649 

650 def __iadd__(self, other): 

651 '''Add B{C{other}} to this instance. 

652 

653 @arg other: An L{Flinear} instance or an iterable of 

654 C{x_ys} values, see method C{fadd_}. 

655 

656 @return: This instance, updated (L{Flinear}). 

657 

658 @raise TypeError: Invalid B{C{other}} or the B{C{other}} 

659 and these C{x} and C{y} accumulators 

660 are not compatible. 

661 

662 @raise ValueError: Invalid or odd-length B{C{other}}. 

663 

664 @see: Method L{Flinear.fadd_}. 

665 ''' 

666 if isinstance(other, Flinear): 

667 if len(other) > 0: 

668 if len(self) > 0: 

669 n = other._n 

670 D = (other._X._M1 - self._X._M1) * \ 

671 (other._Y._M1 - self._Y._M1) * \ 

672 (n * self._n / (self._n + n)) 

673 self._S += other._S + D 

674 self._X += other._X 

675 self._Y += other._Y 

676 self._n += n 

677 else: 

678 self._copy(self, other) 

679 else: 

680 try: 

681 if _xiterable(other): 

682 self.fadd_(*other) 

683 except Exception as X: 

684 op = _SPACE_(self, _iadd_op_, repr(other)) 

685 raise _xError(X, op) 

686 return self 

687 

688 def _copy(self, d, s): 

689 '''(INTERNAL) Copy C{B{d} = B{s}}. 

690 ''' 

691 _xinstanceof(Flinear, d=d, s=s) 

692 d._S = s._S.copy(deep=False) 

693 d._X = s._X.copy(deep=False) 

694 d._Y = s._Y.copy(deep=False) 

695 d._n = s._n 

696 return d 

697 

698 def _Correlation(self, **sample): 

699 '''(INTERNAL) Return the current (sample) correlation as L{Fsum}. 

700 ''' 

701 return self._Sampled(self._X._Stdev(**sample) * 

702 self._Y._Stdev(**sample), **sample) 

703 

704 def fadd(self, xs, ys, sample=False): 

705 '''Accumulate and return the current count. 

706 

707 @arg xs: Iterable of additional C{x} values (each C{scalar}, 

708 an L{Fsum} or L{Fsum2Tuple}). 

709 @arg ys: Iterable of additional C{y} values (each C{scalar}, 

710 an L{Fsum} or L{Fsum2Tuple}). 

711 @kwarg sample: Use C{B{sample}=True} for the I{sample} count 

712 instead of the I{population} count (C{bool}). 

713 

714 @return: Current, running (sample) count (C{int}). 

715 

716 @raise OverflowError: Partial C{2sum} overflow. 

717 

718 @raise TypeError: Invalid B{C{xs}} or B{C{ys}}. 

719 

720 @raise ValueError: Invalid or non-finite B{C{xs}} or B{C{ys}}. 

721 ''' 

722 n = self._n 

723 if xs and ys: 

724 S = self._S 

725 X = self._X 

726 Y = self._Y 

727 for x, y in _zip(_Xs(xs=xs), _Xs(ys=ys)): # PYCHOK strict=True 

728 n1 = n 

729 n += 1 

730 if n1 > 0: 

731 S += (X._M1 - x) * (Y._M1 - y) * (n1 / n) 

732 X += x 

733 Y += y 

734 self._n = n 

735 return _sampled(n, sample) 

736 

737 def fadd_(self, *x_ys, **sample): 

738 '''Accumulate and return the current count. 

739 

740 @arg x_ys: Individual, alternating C{x, y, x, y, ...} values 

741 (each C{scalar}, an L{Fsum} or L{Fsum2Tuple}). 

742 

743 @see: Method C{Flinear.fadd} for further details. 

744 ''' 

745 if isodd(len(x_ys)): 

746 t = _SPACE_(_odd_, typename(len)) 

747 raise _ValueError(t, len(x_ys)) 

748 return self.fadd(x_ys[0::2], x_ys[1::2], **sample) 

749 

750 def fcorrelation(self, **sample): 

751 '''Return the current, running (sample) correlation (C{float}). 

752 

753 @kwarg sample: Use C{B{sample}=True} for the I{sample} correlation 

754 instead of the I{population} correlation (C{bool}). 

755 ''' 

756 return float(self._Correlation(**sample)) 

757 

758 def fintercept(self, **sample): 

759 '''Return the current, running (sample) intercept (C{float}). 

760 

761 @kwarg sample: Use C{B{sample}=True} for the I{sample} intercept 

762 instead of the I{population} intercept (C{bool}). 

763 ''' 

764 return float(self._Intercept(**sample)) 

765 

766 def fslope(self, **sample): 

767 '''Return the current, running (sample) slope (C{float}). 

768 

769 @kwarg sample: Use C{B{sample}=True} for the I{sample} slope 

770 instead of the I{population} slope (C{bool}). 

771 ''' 

772 return float(self._Slope(**sample)) 

773 

774 def _Intercept(self, **sample): 

775 '''(INTERNAL) Return the current (sample) intercept as L{Fsum}. 

776 ''' 

777 return self._Y._M1 - self._X._M1 * self._Slope(**sample) 

778 

779 def _Sampled(self, T, sample=False): 

780 '''(INTERNAL) Compute the sampled or entire population result. 

781 ''' 

782 T *= _sampled(self._n, sample) 

783 return self._S.copy().fdiv(T, raiser=False) if T else T 

784 

785 def _Slope(self, **sample): 

786 '''(INTERNAL) Return the current (sample) slope as L{Fsum}. 

787 ''' 

788 return self._Sampled(self._X._Variance(**sample), **sample) 

789 

790 @property_RO 

791 def x(self): 

792 '''Get the C{x} accumulator (L{Fcook} or L{Fwelford}). 

793 ''' 

794 return self._X # .copy() 

795 

796 @property_RO 

797 def y(self): 

798 '''Get the C{y} accumulator (L{Fcook} or L{Fwelford}). 

799 ''' 

800 return self._Y # .copy() 

801 

802 

803__all__ += _ALL_DOCS(_FstatsBase, _FstatsNamed) 

804 

805# **) MIT License 

806# 

807# Copyright (C) 2021-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

808# 

809# Permission is hereby granted, free of charge, to any person obtaining a 

810# copy of this software and associated documentation files (the "Software"), 

811# to deal in the Software without restriction, including without limitation 

812# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

813# and/or sell copies of the Software, and to permit persons to whom the 

814# Software is furnished to do so, subject to the following conditions: 

815# 

816# The above copyright notice and this permission notice shall be included 

817# in all copies or substantial portions of the Software. 

818# 

819# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

820# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

821# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

822# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

823# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

824# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

825# OTHER DEALINGS IN THE SOFTWARE.