Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import numpy as np 

2 

3# TODO: add plots to weighting functions for online docs. 

4 

5 

6class RobustNorm(object): 

7 """ 

8 The parent class for the norms used for robust regression. 

9 

10 Lays out the methods expected of the robust norms to be used 

11 by statsmodels.RLM. 

12 

13 See Also 

14 -------- 

15 statsmodels.rlm 

16 

17 Notes 

18 ----- 

19 Currently only M-estimators are available. 

20 

21 References 

22 ---------- 

23 PJ Huber. 'Robust Statistics' John Wiley and Sons, Inc., New York, 1981. 

24 

25 DC Montgomery, EA Peck. 'Introduction to Linear Regression Analysis', 

26 John Wiley and Sons, Inc., New York, 2001. 

27 

28 R Venables, B Ripley. 'Modern Applied Statistics in S' 

29 Springer, New York, 2002. 

30 """ 

31 

32 def rho(self, z): 

33 """ 

34 The robust criterion estimator function. 

35 

36 Abstract method: 

37 

38 -2 loglike used in M-estimator 

39 """ 

40 raise NotImplementedError 

41 

42 def psi(self, z): 

43 """ 

44 Derivative of rho. Sometimes referred to as the influence function. 

45 

46 Abstract method: 

47 

48 psi = rho' 

49 """ 

50 raise NotImplementedError 

51 

52 def weights(self, z): 

53 """ 

54 Returns the value of psi(z) / z 

55 

56 Abstract method: 

57 

58 psi(z) / z 

59 """ 

60 raise NotImplementedError 

61 

62 def psi_deriv(self, z): 

63 """ 

64 Derivative of psi. Used to obtain robust covariance matrix. 

65 

66 See statsmodels.rlm for more information. 

67 

68 Abstract method: 

69 

70 psi_derive = psi' 

71 """ 

72 raise NotImplementedError 

73 

74 def __call__(self, z): 

75 """ 

76 Returns the value of estimator rho applied to an input 

77 """ 

78 return self.rho(z) 

79 

80 

81class LeastSquares(RobustNorm): 

82 """ 

83 Least squares rho for M-estimation and its derived functions. 

84 

85 See Also 

86 -------- 

87 statsmodels.robust.norms.RobustNorm 

88 """ 

89 

90 def rho(self, z): 

91 """ 

92 The least squares estimator rho function 

93 

94 Parameters 

95 ---------- 

96 z : ndarray 

97 1d array 

98 

99 Returns 

100 ------- 

101 rho : ndarray 

102 rho(z) = (1/2.)*z**2 

103 """ 

104 

105 return z**2 * 0.5 

106 

107 def psi(self, z): 

108 """ 

109 The psi function for the least squares estimator 

110 

111 The analytic derivative of rho 

112 

113 Parameters 

114 ---------- 

115 z : array_like 

116 1d array 

117 

118 Returns 

119 ------- 

120 psi : ndarray 

121 psi(z) = z 

122 """ 

123 

124 return np.asarray(z) 

125 

126 def weights(self, z): 

127 """ 

128 The least squares estimator weighting function for the IRLS algorithm. 

129 

130 The psi function scaled by the input z 

131 

132 Parameters 

133 ---------- 

134 z : array_like 

135 1d array 

136 

137 Returns 

138 ------- 

139 weights : ndarray 

140 weights(z) = np.ones(z.shape) 

141 """ 

142 

143 z = np.asarray(z) 

144 return np.ones(z.shape, np.float64) 

145 

146 def psi_deriv(self, z): 

147 """ 

148 The derivative of the least squares psi function. 

149 

150 Returns 

151 ------- 

152 psi_deriv : ndarray 

153 ones(z.shape) 

154 

155 Notes 

156 ----- 

157 Used to estimate the robust covariance matrix. 

158 """ 

159 return np.ones(z.shape, np.float64) 

160 

161 

162class HuberT(RobustNorm): 

163 """ 

164 Huber's T for M estimation. 

165 

166 Parameters 

167 ---------- 

168 t : float, optional 

169 The tuning constant for Huber's t function. The default value is 

170 1.345. 

171 

172 See Also 

173 -------- 

174 statsmodels.robust.norms.RobustNorm 

175 """ 

176 

177 def __init__(self, t=1.345): 

178 self.t = t 

179 

180 def _subset(self, z): 

181 """ 

182 Huber's T is defined piecewise over the range for z 

183 """ 

184 z = np.asarray(z) 

185 return np.less_equal(np.abs(z), self.t) 

186 

187 def rho(self, z): 

188 r""" 

189 The robust criterion function for Huber's t. 

190 

191 Parameters 

192 ---------- 

193 z : array_like 

194 1d array 

195 

196 Returns 

197 ------- 

198 rho : ndarray 

199 rho(z) = .5*z**2 for \|z\| <= t 

200 

201 rho(z) = \|z\|*t - .5*t**2 for \|z\| > t 

202 """ 

203 z = np.asarray(z) 

204 test = self._subset(z) 

205 return (test * 0.5 * z**2 + 

206 (1 - test) * (np.abs(z) * self.t - 0.5 * self.t**2)) 

207 

208 def psi(self, z): 

209 r""" 

210 The psi function for Huber's t estimator 

211 

212 The analytic derivative of rho 

213 

214 Parameters 

215 ---------- 

216 z : array_like 

217 1d array 

218 

219 Returns 

220 ------- 

221 psi : ndarray 

222 psi(z) = z for \|z\| <= t 

223 

224 psi(z) = sign(z)*t for \|z\| > t 

225 """ 

226 z = np.asarray(z) 

227 test = self._subset(z) 

228 return test * z + (1 - test) * self.t * np.sign(z) 

229 

230 def weights(self, z): 

231 r""" 

232 Huber's t weighting function for the IRLS algorithm 

233 

234 The psi function scaled by z 

235 

236 Parameters 

237 ---------- 

238 z : array_like 

239 1d array 

240 

241 Returns 

242 ------- 

243 weights : ndarray 

244 weights(z) = 1 for \|z\| <= t 

245 

246 weights(z) = t/\|z\| for \|z\| > t 

247 """ 

248 z = np.asarray(z) 

249 test = self._subset(z) 

250 absz = np.abs(z) 

251 absz[test] = 1.0 

252 return test + (1 - test) * self.t / absz 

253 

254 def psi_deriv(self, z): 

255 """ 

256 The derivative of Huber's t psi function 

257 

258 Notes 

259 ----- 

260 Used to estimate the robust covariance matrix. 

261 """ 

262 return np.less_equal(np.abs(z), self.t) 

263 

264 

265# TODO: untested, but looks right. RamsayE not available in R or SAS? 

266class RamsayE(RobustNorm): 

267 """ 

268 Ramsay's Ea for M estimation. 

269 

270 Parameters 

271 ---------- 

272 a : float, optional 

273 The tuning constant for Ramsay's Ea function. The default value is 

274 0.3. 

275 

276 See Also 

277 -------- 

278 statsmodels.robust.norms.RobustNorm 

279 """ 

280 

281 def __init__(self, a=.3): 

282 self.a = a 

283 

284 def rho(self, z): 

285 r""" 

286 The robust criterion function for Ramsay's Ea. 

287 

288 Parameters 

289 ---------- 

290 z : array_like 

291 1d array 

292 

293 Returns 

294 ------- 

295 rho : ndarray 

296 rho(z) = a**-2 * (1 - exp(-a*\|z\|)*(1 + a*\|z\|)) 

297 """ 

298 z = np.asarray(z) 

299 return (1 - np.exp(-self.a * np.abs(z)) * 

300 (1 + self.a * np.abs(z))) / self.a**2 

301 

302 def psi(self, z): 

303 r""" 

304 The psi function for Ramsay's Ea estimator 

305 

306 The analytic derivative of rho 

307 

308 Parameters 

309 ---------- 

310 z : array_like 

311 1d array 

312 

313 Returns 

314 ------- 

315 psi : ndarray 

316 psi(z) = z*exp(-a*\|z\|) 

317 """ 

318 z = np.asarray(z) 

319 return z * np.exp(-self.a * np.abs(z)) 

320 

321 def weights(self, z): 

322 r""" 

323 Ramsay's Ea weighting function for the IRLS algorithm 

324 

325 The psi function scaled by z 

326 

327 Parameters 

328 ---------- 

329 z : array_like 

330 1d array 

331 

332 Returns 

333 ------- 

334 weights : ndarray 

335 weights(z) = exp(-a*\|z\|) 

336 """ 

337 

338 z = np.asarray(z) 

339 return np.exp(-self.a * np.abs(z)) 

340 

341 def psi_deriv(self, z): 

342 """ 

343 The derivative of Ramsay's Ea psi function. 

344 

345 Notes 

346 ----- 

347 Used to estimate the robust covariance matrix. 

348 """ 

349 a = self.a 

350 x = np.exp(-a * np.abs(z)) 

351 dx = -a * x * np.sign(z) 

352 y = z 

353 dy = 1 

354 return x * dy + y * dx 

355 

356 

357class AndrewWave(RobustNorm): 

358 """ 

359 Andrew's wave for M estimation. 

360 

361 Parameters 

362 ---------- 

363 a : float, optional 

364 The tuning constant for Andrew's Wave function. The default value is 

365 1.339. 

366 

367 See Also 

368 -------- 

369 statsmodels.robust.norms.RobustNorm 

370 """ 

371 def __init__(self, a=1.339): 

372 self.a = a 

373 

374 def _subset(self, z): 

375 """ 

376 Andrew's wave is defined piecewise over the range of z. 

377 """ 

378 z = np.asarray(z) 

379 return np.less_equal(np.abs(z), self.a * np.pi) 

380 

381 def rho(self, z): 

382 r""" 

383 The robust criterion function for Andrew's wave. 

384 

385 Parameters 

386 ---------- 

387 z : array_like 

388 1d array 

389 

390 Returns 

391 ------- 

392 rho : ndarray 

393 rho(z) = a*(1-cos(z/a)) for \|z\| <= a*pi 

394 

395 rho(z) = 2*a for \|z\| > a*pi 

396 """ 

397 

398 a = self.a 

399 z = np.asarray(z) 

400 test = self._subset(z) 

401 return (test * a * (1 - np.cos(z / a)) + 

402 (1 - test) * 2 * a) 

403 

404 def psi(self, z): 

405 r""" 

406 The psi function for Andrew's wave 

407 

408 The analytic derivative of rho 

409 

410 Parameters 

411 ---------- 

412 z : array_like 

413 1d array 

414 

415 Returns 

416 ------- 

417 psi : ndarray 

418 psi(z) = sin(z/a) for \|z\| <= a*pi 

419 

420 psi(z) = 0 for \|z\| > a*pi 

421 """ 

422 

423 a = self.a 

424 z = np.asarray(z) 

425 test = self._subset(z) 

426 return test * np.sin(z / a) 

427 

428 def weights(self, z): 

429 r""" 

430 Andrew's wave weighting function for the IRLS algorithm 

431 

432 The psi function scaled by z 

433 

434 Parameters 

435 ---------- 

436 z : array_like 

437 1d array 

438 

439 Returns 

440 ------- 

441 weights : ndarray 

442 weights(z) = sin(z/a)/(z/a) for \|z\| <= a*pi 

443 

444 weights(z) = 0 for \|z\| > a*pi 

445 """ 

446 a = self.a 

447 z = np.asarray(z) 

448 test = self._subset(z) 

449 ratio = z / a 

450 small = np.abs(ratio) < np.finfo(np.double).eps 

451 if np.any(small): 

452 weights = np.ones_like(ratio) 

453 large = ~small 

454 ratio = ratio[large] 

455 weights[large] = test[large] * np.sin(ratio) / ratio 

456 else: 

457 weights = test * np.sin(ratio) / ratio 

458 return weights 

459 

460 def psi_deriv(self, z): 

461 """ 

462 The derivative of Andrew's wave psi function 

463 

464 Notes 

465 ----- 

466 Used to estimate the robust covariance matrix. 

467 """ 

468 

469 test = self._subset(z) 

470 return test*np.cos(z / self.a)/self.a 

471 

472 

473# TODO: this is untested 

474class TrimmedMean(RobustNorm): 

475 """ 

476 Trimmed mean function for M-estimation. 

477 

478 Parameters 

479 ---------- 

480 c : float, optional 

481 The tuning constant for Ramsay's Ea function. The default value is 

482 2.0. 

483 

484 See Also 

485 -------- 

486 statsmodels.robust.norms.RobustNorm 

487 """ 

488 

489 def __init__(self, c=2.): 

490 self.c = c 

491 

492 def _subset(self, z): 

493 """ 

494 Least trimmed mean is defined piecewise over the range of z. 

495 """ 

496 

497 z = np.asarray(z) 

498 return np.less_equal(np.abs(z), self.c) 

499 

500 def rho(self, z): 

501 r""" 

502 The robust criterion function for least trimmed mean. 

503 

504 Parameters 

505 ---------- 

506 z : array_like 

507 1d array 

508 

509 Returns 

510 ------- 

511 rho : ndarray 

512 rho(z) = (1/2.)*z**2 for \|z\| <= c 

513 

514 rho(z) = 0 for \|z\| > c 

515 """ 

516 

517 z = np.asarray(z) 

518 test = self._subset(z) 

519 return test * z**2 * 0.5 

520 

521 def psi(self, z): 

522 r""" 

523 The psi function for least trimmed mean 

524 

525 The analytic derivative of rho 

526 

527 Parameters 

528 ---------- 

529 z : array_like 

530 1d array 

531 

532 Returns 

533 ------- 

534 psi : ndarray 

535 psi(z) = z for \|z\| <= c 

536 

537 psi(z) = 0 for \|z\| > c 

538 """ 

539 z = np.asarray(z) 

540 test = self._subset(z) 

541 return test * z 

542 

543 def weights(self, z): 

544 r""" 

545 Least trimmed mean weighting function for the IRLS algorithm 

546 

547 The psi function scaled by z 

548 

549 Parameters 

550 ---------- 

551 z : array_like 

552 1d array 

553 

554 Returns 

555 ------- 

556 weights : ndarray 

557 weights(z) = 1 for \|z\| <= c 

558 

559 weights(z) = 0 for \|z\| > c 

560 """ 

561 z = np.asarray(z) 

562 test = self._subset(z) 

563 return test 

564 

565 def psi_deriv(self, z): 

566 """ 

567 The derivative of least trimmed mean psi function 

568 

569 Notes 

570 ----- 

571 Used to estimate the robust covariance matrix. 

572 """ 

573 test = self._subset(z) 

574 return test 

575 

576 

577class Hampel(RobustNorm): 

578 """ 

579 

580 Hampel function for M-estimation. 

581 

582 Parameters 

583 ---------- 

584 a : float, optional 

585 b : float, optional 

586 c : float, optional 

587 The tuning constants for Hampel's function. The default values are 

588 a,b,c = 2, 4, 8. 

589 

590 See Also 

591 -------- 

592 statsmodels.robust.norms.RobustNorm 

593 """ 

594 

595 def __init__(self, a=2., b=4., c=8.): 

596 self.a = a 

597 self.b = b 

598 self.c = c 

599 

600 def _subset(self, z): 

601 """ 

602 Hampel's function is defined piecewise over the range of z 

603 """ 

604 z = np.abs(np.asarray(z)) 

605 t1 = np.less_equal(z, self.a) 

606 t2 = np.less_equal(z, self.b) * np.greater(z, self.a) 

607 t3 = np.less_equal(z, self.c) * np.greater(z, self.b) 

608 return t1, t2, t3 

609 

610 def rho(self, z): 

611 r""" 

612 The robust criterion function for Hampel's estimator 

613 

614 Parameters 

615 ---------- 

616 z : array_like 

617 1d array 

618 

619 Returns 

620 ------- 

621 rho : ndarray 

622 rho(z) = (1/2.)*z**2 for \|z\| <= a 

623 

624 rho(z) = a*\|z\| - 1/2.*a**2 for a < \|z\| <= b 

625 

626 rho(z) = a*(c*\|z\|-(1/2.)*z**2)/(c-b) for b < \|z\| <= c 

627 

628 rho(z) = a*(b + c - a) for \|z\| > c 

629 """ 

630 

631 z = np.abs(z) 

632 a = self.a 

633 b = self.b 

634 c = self.c 

635 t1, t2, t3 = self._subset(z) 

636 v = (t1 * z**2 * 0.5 + 

637 t2 * (a * z - a**2 * 0.5) + 

638 t3 * (a * (c * z - z**2 * 0.5) / (c - b) - 7 * a**2 / 6.) + 

639 (1 - t1 + t2 + t3) * a * (b + c - a)) 

640 return v 

641 

642 def psi(self, z): 

643 r""" 

644 The psi function for Hampel's estimator 

645 

646 The analytic derivative of rho 

647 

648 Parameters 

649 ---------- 

650 z : array_like 

651 1d array 

652 

653 Returns 

654 ------- 

655 psi : ndarray 

656 psi(z) = z for \|z\| <= a 

657 

658 psi(z) = a*sign(z) for a < \|z\| <= b 

659 

660 psi(z) = a*sign(z)*(c - \|z\|)/(c-b) for b < \|z\| <= c 

661 

662 psi(z) = 0 for \|z\| > c 

663 """ 

664 z = np.asarray(z) 

665 a = self.a 

666 b = self.b 

667 c = self.c 

668 t1, t2, t3 = self._subset(z) 

669 s = np.sign(z) 

670 z = np.abs(z) 

671 v = s * (t1 * z + 

672 t2 * a*s + 

673 t3 * a*s * (c - z) / (c - b)) 

674 return v 

675 

676 def weights(self, z): 

677 r""" 

678 Hampel weighting function for the IRLS algorithm 

679 

680 The psi function scaled by z 

681 

682 Parameters 

683 ---------- 

684 z : array_like 

685 1d array 

686 

687 Returns 

688 ------- 

689 weights : ndarray 

690 weights(z) = 1 for \|z\| <= a 

691 

692 weights(z) = a/\|z\| for a < \|z\| <= b 

693 

694 weights(z) = a*(c - \|z\|)/(\|z\|*(c-b)) for b < \|z\| <= c 

695 

696 weights(z) = 0 for \|z\| > c 

697 """ 

698 z = np.asarray(z) 

699 a = self.a 

700 b = self.b 

701 c = self.c 

702 t1, t2, t3 = self._subset(z) 

703 

704 v = np.zeros_like(z) 

705 v[t1] = 1.0 

706 abs_z = np.abs(z) 

707 v[t2] = a / abs_z[t2] 

708 abs_zt3 = abs_z[t3] 

709 v[t3] = a * (c - abs_zt3) / (abs_zt3 * (c - b)) 

710 v[np.where(np.isnan(v))] = 1. # TODO: for some reason 0 returns a nan? 

711 return v 

712 

713 def psi_deriv(self, z): 

714 t1, t2, t3 = self._subset(z) 

715 a, b, c = self.a, self.b, self.c 

716 # default is t1 

717 d = np.zeros_like(z) 

718 d[t1] = 1.0 

719 zt3 = z[t3] 

720 d[t3] = (a * np.sign(zt3) * zt3) / (np.abs(zt3) * (c - b)) 

721 return d 

722 

723 

724class TukeyBiweight(RobustNorm): 

725 """ 

726 

727 Tukey's biweight function for M-estimation. 

728 

729 Parameters 

730 ---------- 

731 c : float, optional 

732 The tuning constant for Tukey's Biweight. The default value is 

733 c = 4.685. 

734 

735 Notes 

736 ----- 

737 Tukey's biweight is sometime's called bisquare. 

738 """ 

739 

740 def __init__(self, c=4.685): 

741 self.c = c 

742 

743 def _subset(self, z): 

744 """ 

745 Tukey's biweight is defined piecewise over the range of z 

746 """ 

747 z = np.abs(np.asarray(z)) 

748 return np.less_equal(z, self.c) 

749 

750 def rho(self, z): 

751 r""" 

752 The robust criterion function for Tukey's biweight estimator 

753 

754 Parameters 

755 ---------- 

756 z : array_like 

757 1d array 

758 

759 Returns 

760 ------- 

761 rho : ndarray 

762 rho(z) = -(1 - (z/c)**2)**3 * c**2/6. for \|z\| <= R 

763 

764 rho(z) = 0 for \|z\| > R 

765 """ 

766 subset = self._subset(z) 

767 return -(1 - (z / self.c)**2)**3 * subset * self.c**2 / 6. 

768 

769 def psi(self, z): 

770 r""" 

771 The psi function for Tukey's biweight estimator 

772 

773 The analytic derivative of rho 

774 

775 Parameters 

776 ---------- 

777 z : array_like 

778 1d array 

779 

780 Returns 

781 ------- 

782 psi : ndarray 

783 psi(z) = z*(1 - (z/c)**2)**2 for \|z\| <= R 

784 

785 psi(z) = 0 for \|z\| > R 

786 """ 

787 

788 z = np.asarray(z) 

789 subset = self._subset(z) 

790 return z * (1 - (z / self.c)**2)**2 * subset 

791 

792 def weights(self, z): 

793 r""" 

794 Tukey's biweight weighting function for the IRLS algorithm 

795 

796 The psi function scaled by z 

797 

798 Parameters 

799 ---------- 

800 z : array_like 

801 1d array 

802 

803 Returns 

804 ------- 

805 weights : ndarray 

806 psi(z) = (1 - (z/c)**2)**2 for \|z\| <= R 

807 

808 psi(z) = 0 for \|z\| > R 

809 """ 

810 

811 subset = self._subset(z) 

812 return (1 - (z / self.c)**2)**2 * subset 

813 

814 def psi_deriv(self, z): 

815 """ 

816 The derivative of Tukey's biweight psi function 

817 

818 Notes 

819 ----- 

820 Used to estimate the robust covariance matrix. 

821 """ 

822 subset = self._subset(z) 

823 return subset * ((1 - (z/self.c)**2)**2 

824 - (4*z**2/self.c**2) * (1-(z/self.c)**2)) 

825 

826 

827def estimate_location(a, scale, norm=None, axis=0, initial=None, 

828 maxiter=30, tol=1.0e-06): 

829 """ 

830 M-estimator of location using self.norm and a current 

831 estimator of scale. 

832 

833 This iteratively finds a solution to 

834 

835 norm.psi((a-mu)/scale).sum() == 0 

836 

837 Parameters 

838 ---------- 

839 a : ndarray 

840 Array over which the location parameter is to be estimated 

841 scale : ndarray 

842 Scale parameter to be used in M-estimator 

843 norm : RobustNorm, optional 

844 Robust norm used in the M-estimator. The default is HuberT(). 

845 axis : int, optional 

846 Axis along which to estimate the location parameter. The default is 0. 

847 initial : ndarray, optional 

848 Initial condition for the location parameter. Default is None, which 

849 uses the median of a. 

850 niter : int, optional 

851 Maximum number of iterations. The default is 30. 

852 tol : float, optional 

853 Toleration for convergence. The default is 1e-06. 

854 

855 Returns 

856 ------- 

857 mu : ndarray 

858 Estimate of location 

859 """ 

860 if norm is None: 

861 norm = HuberT() 

862 

863 if initial is None: 

864 mu = np.median(a, axis) 

865 else: 

866 mu = initial 

867 

868 for iter in range(maxiter): 

869 W = norm.weights((a-mu)/scale) 

870 nmu = np.sum(W*a, axis) / np.sum(W, axis) 

871 if np.alltrue(np.less(np.abs(mu - nmu), scale * tol)): 

872 return nmu 

873 else: 

874 mu = nmu 

875 raise ValueError("location estimator failed to converge in %d iterations" 

876 % maxiter)