owlplanner 2025.2.8__py3-none-any.whl → 2025.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- owlplanner/abcapi.py +15 -15
- owlplanner/config.py +138 -133
- owlplanner/logging.py +13 -13
- owlplanner/plan.py +622 -619
- owlplanner/progress.py +2 -2
- owlplanner/rates.py +72 -72
- owlplanner/tax2025.py +3 -3
- owlplanner/timelists.py +31 -29
- owlplanner/utils.py +9 -9
- owlplanner/version.py +1 -1
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.9.dist-info}/METADATA +1 -1
- owlplanner-2025.2.9.dist-info/RECORD +17 -0
- owlplanner-2025.2.8.dist-info/RECORD +0 -17
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.9.dist-info}/WHEEL +0 -0
- {owlplanner-2025.2.8.dist-info → owlplanner-2025.2.9.dist-info}/licenses/LICENSE +0 -0
owlplanner/plan.py
CHANGED
|
@@ -63,10 +63,10 @@ def _genXi_n(profile, fraction, n_d, N_n, a, b, c):
|
|
|
63
63
|
Series is unadjusted for inflation.
|
|
64
64
|
"""
|
|
65
65
|
xi = np.ones(N_n)
|
|
66
|
-
if profile ==
|
|
66
|
+
if profile == "flat":
|
|
67
67
|
if n_d < N_n:
|
|
68
68
|
xi[n_d:] *= fraction
|
|
69
|
-
elif profile ==
|
|
69
|
+
elif profile == "smile":
|
|
70
70
|
span = N_n - 1 - c
|
|
71
71
|
x = np.linspace(0, span, N_n - c)
|
|
72
72
|
a /= 100
|
|
@@ -82,7 +82,7 @@ def _genXi_n(profile, fraction, n_d, N_n, a, b, c):
|
|
|
82
82
|
xi[n_d:] *= fraction
|
|
83
83
|
xi *= neutralSum / xi.sum()
|
|
84
84
|
else:
|
|
85
|
-
raise ValueError(
|
|
85
|
+
raise ValueError("Unknown profile type %s." % profile)
|
|
86
86
|
|
|
87
87
|
return xi
|
|
88
88
|
|
|
@@ -142,7 +142,7 @@ def clone(plan, newname=None, *, verbose=True, logstreams=None):
|
|
|
142
142
|
newplan.setLogstreams(verbose, logstreams)
|
|
143
143
|
|
|
144
144
|
if newname is None:
|
|
145
|
-
newplan.rename(plan._name +
|
|
145
|
+
newplan.rename(plan._name + " (copy)")
|
|
146
146
|
else:
|
|
147
147
|
newplan.rename(newname)
|
|
148
148
|
|
|
@@ -160,8 +160,8 @@ def _checkCaseStatus(func):
|
|
|
160
160
|
|
|
161
161
|
@wraps(func)
|
|
162
162
|
def wrapper(self, *args, **kwargs):
|
|
163
|
-
if self.caseStatus !=
|
|
164
|
-
self.mylog.vprint(
|
|
163
|
+
if self.caseStatus != "solved":
|
|
164
|
+
self.mylog.vprint("Preventing to run method %s() while case is %s." % (func.__name__, self.caseStatus))
|
|
165
165
|
return None
|
|
166
166
|
return func(self, *args, **kwargs)
|
|
167
167
|
|
|
@@ -177,11 +177,11 @@ def _checkConfiguration(func):
|
|
|
177
177
|
@wraps(func)
|
|
178
178
|
def wrapper(self, *args, **kwargs):
|
|
179
179
|
if self.xi_n is None:
|
|
180
|
-
msg =
|
|
180
|
+
msg = "You must define a spending profile before calling %s()." % func.__name__
|
|
181
181
|
self.mylog.vprint(msg)
|
|
182
182
|
raise RuntimeError(msg)
|
|
183
183
|
if self.alpha_ijkn is None:
|
|
184
|
-
msg =
|
|
184
|
+
msg = "You must define an allocation profile before calling %s()." % func.__name__
|
|
185
185
|
self.mylog.vprint(msg)
|
|
186
186
|
raise RuntimeError(msg)
|
|
187
187
|
return func(self, *args, **kwargs)
|
|
@@ -201,8 +201,9 @@ def _timer(func):
|
|
|
201
201
|
result = func(self, *args, **kwargs)
|
|
202
202
|
pt = time.process_time() - pt0
|
|
203
203
|
rt = time.time() - rt0
|
|
204
|
-
self.mylog.vprint(
|
|
205
|
-
|
|
204
|
+
self.mylog.vprint(
|
|
205
|
+
"CPU time used: %dm%.1fs, Wall time: %dm%.1fs." % (int(pt / 60), pt % 60, int(rt / 60), rt % 60)
|
|
206
|
+
)
|
|
206
207
|
return result
|
|
207
208
|
|
|
208
209
|
return wrapper
|
|
@@ -221,8 +222,8 @@ class Plan(object):
|
|
|
221
222
|
and the third the life expectancy. Last argument is a name for
|
|
222
223
|
the plan.
|
|
223
224
|
"""
|
|
224
|
-
if name ==
|
|
225
|
-
raise ValueError(
|
|
225
|
+
if name == "":
|
|
226
|
+
raise ValueError("Plan must have a name")
|
|
226
227
|
|
|
227
228
|
self._name = name
|
|
228
229
|
self.setLogstreams(verbose, logstreams)
|
|
@@ -235,21 +236,21 @@ class Plan(object):
|
|
|
235
236
|
self.N_z = 2
|
|
236
237
|
|
|
237
238
|
# Default interpolation parameters for allocation ratios.
|
|
238
|
-
self.interpMethod =
|
|
239
|
+
self.interpMethod = "linear"
|
|
239
240
|
self._interpolator = self._linInterp
|
|
240
241
|
self.interpCenter = 15
|
|
241
242
|
self.interpWidth = 5
|
|
242
243
|
|
|
243
|
-
self.defaultPlots =
|
|
244
|
-
self.defaultSolver =
|
|
244
|
+
self.defaultPlots = "nominal"
|
|
245
|
+
self.defaultSolver = "HiGHS"
|
|
245
246
|
|
|
246
247
|
self.N_i = len(yobs)
|
|
247
|
-
assert 0 < self.N_i and self.N_i <= 2,
|
|
248
|
-
assert self.N_i == len(expectancy),
|
|
249
|
-
assert self.N_i == len(inames),
|
|
250
|
-
assert inames[0] !=
|
|
248
|
+
assert 0 < self.N_i and self.N_i <= 2, "Cannot support %d individuals." % self.N_i
|
|
249
|
+
assert self.N_i == len(expectancy), "Expectancy must have %d entries." % self.N_i
|
|
250
|
+
assert self.N_i == len(inames), "Names for individuals must have %d entries." % self.N_i
|
|
251
|
+
assert inames[0] != "" or (self.N_i == 2 and inames[1] == ""), "Name for each individual must be provided."
|
|
251
252
|
|
|
252
|
-
self.filingStatus = [
|
|
253
|
+
self.filingStatus = ["single", "married"][self.N_i - 1]
|
|
253
254
|
|
|
254
255
|
self.inames = inames
|
|
255
256
|
self.yobs = np.array(yobs, dtype=np.int32)
|
|
@@ -300,11 +301,13 @@ class Plan(object):
|
|
|
300
301
|
self.prevMAGI = np.zeros((2))
|
|
301
302
|
|
|
302
303
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
303
|
-
self.mylog.vprint(
|
|
304
|
-
|
|
304
|
+
self.mylog.vprint(
|
|
305
|
+
"Preparing scenario of %d years for %d individual%s." % (self.N_n, self.N_i, ["", "s"][self.N_i - 1])
|
|
306
|
+
)
|
|
305
307
|
for i in range(self.N_i):
|
|
306
|
-
self.mylog.vprint(
|
|
307
|
-
|
|
308
|
+
self.mylog.vprint(
|
|
309
|
+
"%14s: life horizon from %d -> %d." % (self.inames[i], thisyear, thisyear + self.horizons[i] - 1)
|
|
310
|
+
)
|
|
308
311
|
|
|
309
312
|
# Prepare income tax and RMD time series.
|
|
310
313
|
self.rho_in = tx.rho_in(self.yobs, self.N_n)
|
|
@@ -320,11 +323,11 @@ class Plan(object):
|
|
|
320
323
|
self.timeListsFileName = "None"
|
|
321
324
|
self.timeLists = {}
|
|
322
325
|
self.zeroContributions()
|
|
323
|
-
self.caseStatus =
|
|
326
|
+
self.caseStatus = "unsolved"
|
|
324
327
|
self.rateMethod = None
|
|
325
328
|
|
|
326
329
|
self.ARCoord = None
|
|
327
|
-
self.objective =
|
|
330
|
+
self.objective = "unknown"
|
|
328
331
|
|
|
329
332
|
# Placeholders to check if properly configured.
|
|
330
333
|
self.xi_n = None
|
|
@@ -361,13 +364,13 @@ class Plan(object):
|
|
|
361
364
|
thisyear = date.today().year
|
|
362
365
|
|
|
363
366
|
if isinstance(mydate, date):
|
|
364
|
-
mydate = mydate.strftime(
|
|
367
|
+
mydate = mydate.strftime("%Y-%m-%d")
|
|
365
368
|
|
|
366
|
-
if mydate is None or mydate ==
|
|
369
|
+
if mydate is None or mydate == "today":
|
|
367
370
|
refdate = date.today()
|
|
368
|
-
self.startDate = refdate.strftime(
|
|
371
|
+
self.startDate = refdate.strftime("%Y-%m-%d")
|
|
369
372
|
else:
|
|
370
|
-
mydatelist = mydate.split(
|
|
373
|
+
mydatelist = mydate.split("-")
|
|
371
374
|
if len(mydatelist) == 2 or len(mydatelist) == 3:
|
|
372
375
|
self.startDate = mydate
|
|
373
376
|
# Ignore the year provided.
|
|
@@ -379,7 +382,7 @@ class Plan(object):
|
|
|
379
382
|
# Take midnight as the reference.
|
|
380
383
|
self.yearFracLeft = 1 - (refdate.timetuple().tm_yday - 1) / (365 + lp)
|
|
381
384
|
|
|
382
|
-
self.mylog.vprint(
|
|
385
|
+
self.mylog.vprint("Setting 1st-year starting date to %s." % (self.startDate))
|
|
383
386
|
|
|
384
387
|
return None
|
|
385
388
|
|
|
@@ -390,11 +393,11 @@ class Plan(object):
|
|
|
390
393
|
if value is None:
|
|
391
394
|
return self.defaultPlots
|
|
392
395
|
|
|
393
|
-
opts = [
|
|
396
|
+
opts = ["nominal", "today"]
|
|
394
397
|
if value in opts:
|
|
395
398
|
return value
|
|
396
399
|
|
|
397
|
-
raise ValueError(
|
|
400
|
+
raise ValueError("Value type must be one of: %r" % opts)
|
|
398
401
|
|
|
399
402
|
return None
|
|
400
403
|
|
|
@@ -404,7 +407,7 @@ class Plan(object):
|
|
|
404
407
|
to distinguish graph outputs and as base name for
|
|
405
408
|
saving configurations and workbooks.
|
|
406
409
|
"""
|
|
407
|
-
self.mylog.vprint(
|
|
410
|
+
self.mylog.vprint("Renaming plan %s -> %s." % (self._name, newname))
|
|
408
411
|
self._name = newname
|
|
409
412
|
|
|
410
413
|
def setSpousalDepositFraction(self, eta):
|
|
@@ -417,13 +420,13 @@ class Plan(object):
|
|
|
417
420
|
where s_n is the surplus amount. Here d_0n is the taxable account
|
|
418
421
|
deposit for the first spouse while d_1n is for the second spouse.
|
|
419
422
|
"""
|
|
420
|
-
assert 0 <= eta and eta <= 1,
|
|
423
|
+
assert 0 <= eta and eta <= 1, "Fraction must be between 0 and 1."
|
|
421
424
|
if self.N_i != 2:
|
|
422
|
-
self.mylog.vprint(
|
|
425
|
+
self.mylog.vprint("Deposit fraction can only be 0 for single individuals.")
|
|
423
426
|
eta = 0
|
|
424
427
|
else:
|
|
425
|
-
self.mylog.vprint(
|
|
426
|
-
self.mylog.vprint(
|
|
428
|
+
self.mylog.vprint("Setting spousal surplus deposit fraction to %.1f." % eta)
|
|
429
|
+
self.mylog.vprint("\t%s: %.1f, %s: %.1f" % (self.inames[0], (1 - eta), self.inames[1], eta))
|
|
427
430
|
self.eta = eta
|
|
428
431
|
|
|
429
432
|
def setDefaultPlots(self, value):
|
|
@@ -432,69 +435,69 @@ class Plan(object):
|
|
|
432
435
|
"""
|
|
433
436
|
|
|
434
437
|
self.defaultPlots = self._checkValue(value)
|
|
435
|
-
self.mylog.vprint(
|
|
438
|
+
self.mylog.vprint("Setting plots default value to %s." % value)
|
|
436
439
|
|
|
437
440
|
def setDividendRate(self, mu):
|
|
438
441
|
"""
|
|
439
442
|
Set dividend rate on equities. Rate is in percent. Default 2%.
|
|
440
443
|
"""
|
|
441
|
-
assert 0 <= mu and mu <= 100,
|
|
444
|
+
assert 0 <= mu and mu <= 100, "Rate must be between 0 and 100."
|
|
442
445
|
mu /= 100
|
|
443
|
-
self.mylog.vprint(
|
|
446
|
+
self.mylog.vprint("Dividend return rate on equities set to %s." % u.pc(mu, f=1))
|
|
444
447
|
self.mu = mu
|
|
445
|
-
self.caseStatus =
|
|
448
|
+
self.caseStatus = "modified"
|
|
446
449
|
|
|
447
450
|
def setLongTermCapitalTaxRate(self, psi):
|
|
448
451
|
"""
|
|
449
452
|
Set long-term income tax rate. Rate is in percent. Default 15%.
|
|
450
453
|
"""
|
|
451
|
-
assert 0 <= psi and psi <= 100,
|
|
454
|
+
assert 0 <= psi and psi <= 100, "Rate must be between 0 and 100."
|
|
452
455
|
psi /= 100
|
|
453
|
-
self.mylog.vprint(
|
|
456
|
+
self.mylog.vprint("Long-term capital gain income tax set to %s." % u.pc(psi, f=0))
|
|
454
457
|
self.psi = psi
|
|
455
|
-
self.caseStatus =
|
|
458
|
+
self.caseStatus = "modified"
|
|
456
459
|
|
|
457
460
|
def setBeneficiaryFractions(self, phi):
|
|
458
461
|
"""
|
|
459
462
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
460
463
|
Default is [1, 1, 1] for taxable, tax-deferred, adn tax-exempt accounts.
|
|
461
464
|
"""
|
|
462
|
-
assert len(phi) == self.N_j,
|
|
465
|
+
assert len(phi) == self.N_j, "Fractions must have %d entries." % self.N_j
|
|
463
466
|
for j in range(self.N_j):
|
|
464
|
-
assert 0 <= phi[j] <= 1,
|
|
467
|
+
assert 0 <= phi[j] <= 1, "Fractions must be between 0 and 1."
|
|
465
468
|
|
|
466
469
|
self.phi_j = np.array(phi, dtype=np.float32)
|
|
467
|
-
self.mylog.vprint(
|
|
468
|
-
self.caseStatus =
|
|
470
|
+
self.mylog.vprint("Spousal beneficiary fractions set to", phi)
|
|
471
|
+
self.caseStatus = "modified"
|
|
469
472
|
|
|
470
473
|
if np.any(self.phi_j != 1):
|
|
471
|
-
self.mylog.vprint(
|
|
472
|
-
self.mylog.vprint(
|
|
474
|
+
self.mylog.vprint("Consider changing spousal deposit fraction for better convergence.")
|
|
475
|
+
self.mylog.vprint("\tRecommended: setSpousalDepositFraction(%d)" % self.i_d)
|
|
473
476
|
|
|
474
477
|
def setHeirsTaxRate(self, nu):
|
|
475
478
|
"""
|
|
476
479
|
Set the heirs tax rate on the tax-deferred portion of the estate.
|
|
477
480
|
Rate is in percent. Default is 30%.
|
|
478
481
|
"""
|
|
479
|
-
assert 0 <= nu and nu <= 100,
|
|
482
|
+
assert 0 <= nu and nu <= 100, "Rate must be between 0 and 100."
|
|
480
483
|
nu /= 100
|
|
481
|
-
self.mylog.vprint(
|
|
484
|
+
self.mylog.vprint("Heirs tax rate on tax-deferred portion of estate set to %s." % u.pc(nu, f=0))
|
|
482
485
|
self.nu = nu
|
|
483
|
-
self.caseStatus =
|
|
486
|
+
self.caseStatus = "modified"
|
|
484
487
|
|
|
485
|
-
def setPension(self, amounts, ages, indexed=[False, False], units=
|
|
488
|
+
def setPension(self, amounts, ages, indexed=[False, False], units="k"):
|
|
486
489
|
"""
|
|
487
490
|
Set value of pension for each individual and commencement age.
|
|
488
491
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
489
492
|
"""
|
|
490
|
-
assert len(amounts) == self.N_i,
|
|
491
|
-
assert len(ages) == self.N_i,
|
|
492
|
-
assert len(indexed) >= self.N_i,
|
|
493
|
+
assert len(amounts) == self.N_i, "Amounts must have %d entries." % self.N_i
|
|
494
|
+
assert len(ages) == self.N_i, "Ages must have %d entries." % self.N_i
|
|
495
|
+
assert len(indexed) >= self.N_i, "Indexed list must have at least %d entries." % self.N_i
|
|
493
496
|
|
|
494
497
|
fac = u.getUnits(units)
|
|
495
498
|
amounts = u.rescale(amounts, fac)
|
|
496
499
|
|
|
497
|
-
self.mylog.vprint(
|
|
500
|
+
self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)], "at age(s)", ages)
|
|
498
501
|
|
|
499
502
|
thisyear = date.today().year
|
|
500
503
|
# Use zero array freshly initialized.
|
|
@@ -511,24 +514,24 @@ class Plan(object):
|
|
|
511
514
|
self.pensionAmounts = np.array(amounts)
|
|
512
515
|
self.pensionAges = np.array(ages, dtype=np.int32)
|
|
513
516
|
self.pensionIndexed = indexed
|
|
514
|
-
self.caseStatus =
|
|
517
|
+
self.caseStatus = "modified"
|
|
515
518
|
self._adjustedParameters = False
|
|
516
519
|
|
|
517
|
-
def setSocialSecurity(self, amounts, ages, units=
|
|
520
|
+
def setSocialSecurity(self, amounts, ages, units="k"):
|
|
518
521
|
"""
|
|
519
522
|
Set value of social security for each individual and commencement age.
|
|
520
523
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
521
524
|
"""
|
|
522
|
-
assert len(amounts) == self.N_i,
|
|
523
|
-
assert len(ages) == self.N_i,
|
|
525
|
+
assert len(amounts) == self.N_i, "Amounts must have %d entries." % self.N_i
|
|
526
|
+
assert len(ages) == self.N_i, "Ages must have %d entries." % self.N_i
|
|
524
527
|
|
|
525
528
|
fac = u.getUnits(units)
|
|
526
529
|
amounts = u.rescale(amounts, fac)
|
|
527
530
|
|
|
528
531
|
self.mylog.vprint(
|
|
529
|
-
|
|
532
|
+
"Setting social security benefits of",
|
|
530
533
|
[u.d(amounts[i]) for i in range(self.N_i)],
|
|
531
|
-
|
|
534
|
+
"at age(s)",
|
|
532
535
|
ages,
|
|
533
536
|
)
|
|
534
537
|
|
|
@@ -544,11 +547,11 @@ class Plan(object):
|
|
|
544
547
|
|
|
545
548
|
if self.N_i == 2:
|
|
546
549
|
# Approximate calculation for spousal benefit (only valid at FRA).
|
|
547
|
-
self.zeta_in[self.i_s, self.n_d:] = max(amounts[self.i_s], amounts[self.i_d])
|
|
550
|
+
self.zeta_in[self.i_s, self.n_d :] = max(amounts[self.i_s], amounts[self.i_d])
|
|
548
551
|
|
|
549
552
|
self.ssecAmounts = np.array(amounts)
|
|
550
553
|
self.ssecAges = np.array(ages, dtype=np.int32)
|
|
551
|
-
self.caseStatus =
|
|
554
|
+
self.caseStatus = "modified"
|
|
552
555
|
self._adjustedParameters = False
|
|
553
556
|
|
|
554
557
|
def setSpendingProfile(self, profile, percent=60, dip=15, increase=12, delay=0):
|
|
@@ -557,16 +560,16 @@ class Plan(object):
|
|
|
557
560
|
as a second argument. Default value is 60%.
|
|
558
561
|
Dip and increase are percent changes in the smile profile.
|
|
559
562
|
"""
|
|
560
|
-
assert 0 <= percent and percent <= 100,
|
|
561
|
-
assert 0 <= dip and dip <= 100,
|
|
562
|
-
assert -100 <= increase and increase <= 100,
|
|
563
|
-
assert 0 <= delay and delay <= self.N_n - 2
|
|
563
|
+
assert 0 <= percent and percent <= 100, "Survivor value %r outside range." % percent
|
|
564
|
+
assert 0 <= dip and dip <= 100, "Dip value %r outside range." % dip
|
|
565
|
+
assert -100 <= increase and increase <= 100, "Increase value %r outside range." % dip
|
|
566
|
+
assert 0 <= delay and delay <= self.N_n - 2, "Delay value %r outside range." % delay
|
|
564
567
|
|
|
565
568
|
self.chi = percent / 100
|
|
566
569
|
|
|
567
|
-
self.mylog.vprint(
|
|
570
|
+
self.mylog.vprint("Setting", profile, "spending profile.")
|
|
568
571
|
if self.N_i == 2:
|
|
569
|
-
self.mylog.vprint(
|
|
572
|
+
self.mylog.vprint("Securing", u.pc(self.chi, f=0), "of spending amount for surviving spouse.")
|
|
570
573
|
|
|
571
574
|
self.xi_n = _genXi_n(profile, self.chi, self.n_d, self.N_n, dip, increase, delay)
|
|
572
575
|
# Account for time elapsed in the current year.
|
|
@@ -576,7 +579,7 @@ class Plan(object):
|
|
|
576
579
|
self.smileDip = dip
|
|
577
580
|
self.smileIncrease = increase
|
|
578
581
|
self.smileDelay = delay
|
|
579
|
-
self.caseStatus =
|
|
582
|
+
self.caseStatus = "modified"
|
|
580
583
|
|
|
581
584
|
def setRates(self, method, frm=None, to=None, values=None, stdev=None, corr=None):
|
|
582
585
|
"""
|
|
@@ -604,11 +607,11 @@ class Plan(object):
|
|
|
604
607
|
self.rateTo = to
|
|
605
608
|
self.tau_kn = dr.genSeries(self.N_n).transpose()
|
|
606
609
|
self.mylog.vprint(
|
|
607
|
-
|
|
610
|
+
"Generating rate series of",
|
|
608
611
|
len(self.tau_kn[0]),
|
|
609
|
-
|
|
612
|
+
"years using",
|
|
610
613
|
method,
|
|
611
|
-
|
|
614
|
+
"method.",
|
|
612
615
|
)
|
|
613
616
|
|
|
614
617
|
# Account for how late we are now in the first year and reduce rate accordingly.
|
|
@@ -617,7 +620,7 @@ class Plan(object):
|
|
|
617
620
|
# Once rates are selected, (re)build cumulative inflation multipliers.
|
|
618
621
|
self.gamma_n = _genGamma_n(self.tau_kn)
|
|
619
622
|
self._adjustedParameters = False
|
|
620
|
-
self.caseStatus =
|
|
623
|
+
self.caseStatus = "modified"
|
|
621
624
|
|
|
622
625
|
def regenRates(self):
|
|
623
626
|
"""
|
|
@@ -654,24 +657,24 @@ class Plan(object):
|
|
|
654
657
|
to the beginning of the year provided.
|
|
655
658
|
"""
|
|
656
659
|
if self.rateMethod is None:
|
|
657
|
-
raise RuntimeError(
|
|
660
|
+
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
658
661
|
|
|
659
662
|
thisyear = date.today().year
|
|
660
|
-
assert year > thisyear,
|
|
663
|
+
assert year > thisyear, "Internal error in forwardValue()."
|
|
661
664
|
span = year - thisyear
|
|
662
665
|
|
|
663
666
|
return amount * self.gamma_n[span]
|
|
664
667
|
|
|
665
|
-
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units=
|
|
668
|
+
def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units="k"):
|
|
666
669
|
"""
|
|
667
670
|
Three lists containing the balance of all assets in each category for
|
|
668
671
|
each spouse. For single individuals, these lists will contain only
|
|
669
672
|
one entry. Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
670
673
|
"""
|
|
671
|
-
plurals = [
|
|
672
|
-
assert len(taxable) == self.N_i,
|
|
673
|
-
assert len(taxDeferred) == self.N_i,
|
|
674
|
-
assert len(taxFree) == self.N_i,
|
|
674
|
+
plurals = ["", "y", "ies"]
|
|
675
|
+
assert len(taxable) == self.N_i, "taxable must have %d entr%s." % (self.N_i, plurals[self.N_i])
|
|
676
|
+
assert len(taxDeferred) == self.N_i, "taxDeferred must have %d entr%s." % (self.N_i, plurals[self.N_i])
|
|
677
|
+
assert len(taxFree) == self.N_i, "taxFree must have %d entr%s." % (self.N_i, plurals[self.N_i])
|
|
675
678
|
|
|
676
679
|
fac = u.getUnits(units)
|
|
677
680
|
taxable = u.rescale(taxable, fac)
|
|
@@ -683,14 +686,14 @@ class Plan(object):
|
|
|
683
686
|
self.b_ji[1][:] = taxDeferred
|
|
684
687
|
self.b_ji[2][:] = taxFree
|
|
685
688
|
self.beta_ij = self.b_ji.transpose()
|
|
686
|
-
self.caseStatus =
|
|
689
|
+
self.caseStatus = "modified"
|
|
687
690
|
|
|
688
|
-
self.mylog.vprint(
|
|
689
|
-
self.mylog.vprint(
|
|
690
|
-
self.mylog.vprint(
|
|
691
|
-
self.mylog.vprint(
|
|
691
|
+
self.mylog.vprint("Taxable balances:", *[u.d(taxable[i]) for i in range(self.N_i)])
|
|
692
|
+
self.mylog.vprint("Tax-deferred balances:", *[u.d(taxDeferred[i]) for i in range(self.N_i)])
|
|
693
|
+
self.mylog.vprint("Tax-free balances:", *[u.d(taxFree[i]) for i in range(self.N_i)])
|
|
694
|
+
self.mylog.vprint("Sum of all savings accounts:", u.d(np.sum(taxable) + np.sum(taxDeferred) + np.sum(taxFree)))
|
|
692
695
|
self.mylog.vprint(
|
|
693
|
-
|
|
696
|
+
"Post-tax total wealth of approximately",
|
|
694
697
|
u.d(np.sum(taxable) + 0.7 * np.sum(taxDeferred) + np.sum(taxFree)),
|
|
695
698
|
)
|
|
696
699
|
|
|
@@ -706,19 +709,19 @@ class Plan(object):
|
|
|
706
709
|
5 years. This means that the transition from initial to final
|
|
707
710
|
will start occuring in 10 years (15-5) and will end in 20 years (15+5).
|
|
708
711
|
"""
|
|
709
|
-
if method ==
|
|
712
|
+
if method == "linear":
|
|
710
713
|
self._interpolator = self._linInterp
|
|
711
|
-
elif method ==
|
|
714
|
+
elif method == "s-curve":
|
|
712
715
|
self._interpolator = self._tanhInterp
|
|
713
716
|
self.interpCenter = center
|
|
714
717
|
self.interpWidth = width
|
|
715
718
|
else:
|
|
716
|
-
raise ValueError(
|
|
719
|
+
raise ValueError("Method %s not supported." % method)
|
|
717
720
|
|
|
718
721
|
self.interpMethod = method
|
|
719
|
-
self.caseStatus =
|
|
722
|
+
self.caseStatus = "modified"
|
|
720
723
|
|
|
721
|
-
self.mylog.vprint(
|
|
724
|
+
self.mylog.vprint("Asset allocation interpolation method set to %s." % method)
|
|
722
725
|
|
|
723
726
|
def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
|
|
724
727
|
"""
|
|
@@ -744,34 +747,34 @@ class Plan(object):
|
|
|
744
747
|
"""
|
|
745
748
|
self.boundsAR = {}
|
|
746
749
|
self.alpha_ijkn = np.zeros((self.N_i, self.N_j, self.N_k, self.N_n + 1))
|
|
747
|
-
if allocType ==
|
|
750
|
+
if allocType == "account":
|
|
748
751
|
# Make sure we have proper input.
|
|
749
752
|
for item in [taxable, taxDeferred, taxFree]:
|
|
750
|
-
assert len(item) == self.N_i,
|
|
753
|
+
assert len(item) == self.N_i, "%s must one entry per individual." % (item)
|
|
751
754
|
for i in range(self.N_i):
|
|
752
755
|
# Initial and final.
|
|
753
|
-
assert len(item[i]) == 2,
|
|
756
|
+
assert len(item[i]) == 2, "%s[%d] must have 2 lists (initial and final)." % (
|
|
754
757
|
item,
|
|
755
758
|
i,
|
|
756
759
|
)
|
|
757
760
|
for z in range(2):
|
|
758
|
-
assert len(item[i][z]) == self.N_k,
|
|
761
|
+
assert len(item[i][z]) == self.N_k, "%s[%d][%d] must have %d entries." % (
|
|
759
762
|
item,
|
|
760
763
|
i,
|
|
761
764
|
z,
|
|
762
765
|
self.N_k,
|
|
763
766
|
)
|
|
764
|
-
assert abs(sum(item[i][z]) - 100) < 0.01,
|
|
767
|
+
assert abs(sum(item[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
765
768
|
|
|
766
769
|
for i in range(self.N_i):
|
|
767
770
|
self.mylog.vprint(
|
|
768
771
|
self.inames[i],
|
|
769
|
-
|
|
772
|
+
": Setting gliding allocation ratios (%) to",
|
|
770
773
|
allocType,
|
|
771
774
|
)
|
|
772
|
-
self.mylog.vprint(
|
|
773
|
-
self.mylog.vprint(
|
|
774
|
-
self.mylog.vprint(
|
|
775
|
+
self.mylog.vprint(" taxable:", taxable[i][0], "->", taxable[i][1])
|
|
776
|
+
self.mylog.vprint(" taxDeferred:", taxDeferred[i][0], "->", taxDeferred[i][1])
|
|
777
|
+
self.mylog.vprint(" taxFree:", taxFree[i][0], "->", taxFree[i][1])
|
|
775
778
|
|
|
776
779
|
# Order in alpha is j, i, 0/1, k.
|
|
777
780
|
alpha = {}
|
|
@@ -787,30 +790,30 @@ class Plan(object):
|
|
|
787
790
|
dat = self._interpolator(start, end, Nin)
|
|
788
791
|
self.alpha_ijkn[i, j, k, :Nin] = dat[:]
|
|
789
792
|
|
|
790
|
-
self.boundsAR[
|
|
791
|
-
self.boundsAR[
|
|
792
|
-
self.boundsAR[
|
|
793
|
+
self.boundsAR["taxable"] = taxable
|
|
794
|
+
self.boundsAR["tax-deferred"] = taxDeferred
|
|
795
|
+
self.boundsAR["tax-free"] = taxFree
|
|
793
796
|
|
|
794
|
-
elif allocType ==
|
|
795
|
-
assert len(generic) == self.N_i,
|
|
797
|
+
elif allocType == "individual":
|
|
798
|
+
assert len(generic) == self.N_i, "generic must have one list per individual."
|
|
796
799
|
for i in range(self.N_i):
|
|
797
800
|
# Initial and final.
|
|
798
|
-
assert len(generic[i]) == 2,
|
|
801
|
+
assert len(generic[i]) == 2, "generic[%d] must have 2 lists (initial and final)." % i
|
|
799
802
|
for z in range(2):
|
|
800
|
-
assert len(generic[i][z]) == self.N_k,
|
|
803
|
+
assert len(generic[i][z]) == self.N_k, "generic[%d][%d] must have %d entries." % (
|
|
801
804
|
i,
|
|
802
805
|
z,
|
|
803
806
|
self.N_k,
|
|
804
807
|
)
|
|
805
|
-
assert abs(sum(generic[i][z]) - 100) < 0.01,
|
|
808
|
+
assert abs(sum(generic[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
806
809
|
|
|
807
810
|
for i in range(self.N_i):
|
|
808
811
|
self.mylog.vprint(
|
|
809
812
|
self.inames[i],
|
|
810
|
-
|
|
813
|
+
": Setting gliding allocation ratios (%) to",
|
|
811
814
|
allocType,
|
|
812
815
|
)
|
|
813
|
-
self.mylog.vprint(
|
|
816
|
+
self.mylog.vprint("\t", generic[i][0], "->", generic[i][1])
|
|
814
817
|
|
|
815
818
|
for i in range(self.N_i):
|
|
816
819
|
Nin = self.horizons[i] + 1
|
|
@@ -821,19 +824,19 @@ class Plan(object):
|
|
|
821
824
|
for j in range(self.N_j):
|
|
822
825
|
self.alpha_ijkn[i, j, k, :Nin] = dat[:]
|
|
823
826
|
|
|
824
|
-
self.boundsAR[
|
|
827
|
+
self.boundsAR["generic"] = generic
|
|
825
828
|
|
|
826
|
-
elif allocType ==
|
|
827
|
-
assert len(generic) == 2,
|
|
829
|
+
elif allocType == "spouses":
|
|
830
|
+
assert len(generic) == 2, "generic must have 2 entries (initial and final)."
|
|
828
831
|
for z in range(2):
|
|
829
|
-
assert len(generic[z]) == self.N_k,
|
|
832
|
+
assert len(generic[z]) == self.N_k, "generic[%d] must have %d entries." % (
|
|
830
833
|
z,
|
|
831
834
|
self.N_k,
|
|
832
835
|
)
|
|
833
|
-
assert abs(sum(generic[z]) - 100) < 0.01,
|
|
836
|
+
assert abs(sum(generic[z]) - 100) < 0.01, "Sum of percentages must add to 100."
|
|
834
837
|
|
|
835
|
-
self.mylog.vprint(
|
|
836
|
-
self.mylog.vprint(
|
|
838
|
+
self.mylog.vprint("Setting gliding allocation ratios (%) to", allocType)
|
|
839
|
+
self.mylog.vprint("\t", generic[0], "->", generic[1])
|
|
837
840
|
|
|
838
841
|
# Use longest-lived spouse for both time scales.
|
|
839
842
|
Nxn = max(self.horizons) + 1
|
|
@@ -846,12 +849,12 @@ class Plan(object):
|
|
|
846
849
|
for j in range(self.N_j):
|
|
847
850
|
self.alpha_ijkn[i, j, k, :Nxn] = dat[:]
|
|
848
851
|
|
|
849
|
-
self.boundsAR[
|
|
852
|
+
self.boundsAR["generic"] = generic
|
|
850
853
|
|
|
851
854
|
self.ARCoord = allocType
|
|
852
|
-
self.caseStatus =
|
|
855
|
+
self.caseStatus = "modified"
|
|
853
856
|
|
|
854
|
-
self.mylog.vprint(
|
|
857
|
+
self.mylog.vprint("Interpolating assets allocation ratios using", self.interpMethod, "method.")
|
|
855
858
|
|
|
856
859
|
def readContributions(self, filename):
|
|
857
860
|
"""
|
|
@@ -877,7 +880,7 @@ class Plan(object):
|
|
|
877
880
|
try:
|
|
878
881
|
filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
|
|
879
882
|
except Exception as e:
|
|
880
|
-
raise Exception(
|
|
883
|
+
raise Exception("Unsuccessful read of contributions: %s" % e)
|
|
881
884
|
return False
|
|
882
885
|
|
|
883
886
|
self.timeListsFileName = filename
|
|
@@ -893,20 +896,20 @@ class Plan(object):
|
|
|
893
896
|
# Now fill in parameters which are in $.
|
|
894
897
|
for i, iname in enumerate(self.inames):
|
|
895
898
|
h = self.horizons[i]
|
|
896
|
-
self.omega_in[i, :h] = self.timeLists[iname][
|
|
897
|
-
self.kappa_ijn[i, 0, :h] = self.timeLists[iname][
|
|
898
|
-
self.kappa_ijn[i, 1, :h] = self.timeLists[iname][
|
|
899
|
-
self.kappa_ijn[i, 2, :h] = self.timeLists[iname][
|
|
900
|
-
self.kappa_ijn[i, 1, :h] += self.timeLists[iname][
|
|
901
|
-
self.kappa_ijn[i, 2, :h] += self.timeLists[iname][
|
|
902
|
-
self.myRothX_in[i, :h] = self.timeLists[iname][
|
|
903
|
-
self.Lambda_in[i, :h] = self.timeLists[iname][
|
|
899
|
+
self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[:h]
|
|
900
|
+
self.kappa_ijn[i, 0, :h] = self.timeLists[iname]["taxable ctrb"].iloc[:h]
|
|
901
|
+
self.kappa_ijn[i, 1, :h] = self.timeLists[iname]["401k ctrb"].iloc[:h]
|
|
902
|
+
self.kappa_ijn[i, 2, :h] = self.timeLists[iname]["Roth 401k ctrb"].iloc[:h]
|
|
903
|
+
self.kappa_ijn[i, 1, :h] += self.timeLists[iname]["IRA ctrb"].iloc[:h]
|
|
904
|
+
self.kappa_ijn[i, 2, :h] += self.timeLists[iname]["Roth IRA ctrb"].iloc[:h]
|
|
905
|
+
self.myRothX_in[i, :h] = self.timeLists[iname]["Roth conv"].iloc[:h]
|
|
906
|
+
self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[:h]
|
|
904
907
|
|
|
905
908
|
# In 1st year, reduce wages and contributions depending on starting date.
|
|
906
909
|
self.omega_in[:, 0] *= self.yearFracLeft
|
|
907
910
|
self.kappa_ijn[:, :, 0] *= self.yearFracLeft
|
|
908
911
|
|
|
909
|
-
self.caseStatus =
|
|
912
|
+
self.caseStatus = "modified"
|
|
910
913
|
|
|
911
914
|
return self.timeLists
|
|
912
915
|
|
|
@@ -917,14 +920,14 @@ class Plan(object):
|
|
|
917
920
|
if self.timeLists is None:
|
|
918
921
|
return None
|
|
919
922
|
|
|
920
|
-
self.mylog.vprint(
|
|
923
|
+
self.mylog.vprint("Preparing wages and contributions workbook.")
|
|
921
924
|
|
|
922
925
|
def fillsheet(sheet, i):
|
|
923
926
|
sheet.title = self.inames[i]
|
|
924
927
|
df = self.timeLists[self.inames[i]]
|
|
925
928
|
for row in dataframe_to_rows(df, index=False, header=True):
|
|
926
929
|
sheet.append(row)
|
|
927
|
-
_formatSpreadsheet(sheet,
|
|
930
|
+
_formatSpreadsheet(sheet, "currency")
|
|
928
931
|
|
|
929
932
|
wb = Workbook()
|
|
930
933
|
ws = wb.active
|
|
@@ -940,23 +943,32 @@ class Plan(object):
|
|
|
940
943
|
"""
|
|
941
944
|
Reset all contributions variables to zero.
|
|
942
945
|
"""
|
|
943
|
-
self.mylog.vprint(
|
|
946
|
+
self.mylog.vprint("Resetting wages and contributions to zero.")
|
|
944
947
|
|
|
945
948
|
# Reset parameters with zeros.
|
|
946
|
-
self.omega_in[:, :] = 0.
|
|
947
|
-
self.Lambda_in[:, :] = 0.
|
|
948
|
-
self.myRothX_in[:, :] = 0.
|
|
949
|
-
self.kappa_ijn[:, :, :] = 0.
|
|
950
|
-
|
|
951
|
-
cols = [
|
|
952
|
-
|
|
949
|
+
self.omega_in[:, :] = 0.0
|
|
950
|
+
self.Lambda_in[:, :] = 0.0
|
|
951
|
+
self.myRothX_in[:, :] = 0.0
|
|
952
|
+
self.kappa_ijn[:, :, :] = 0.0
|
|
953
|
+
|
|
954
|
+
cols = [
|
|
955
|
+
"year",
|
|
956
|
+
"anticipated wages",
|
|
957
|
+
"taxable ctrb",
|
|
958
|
+
"401k ctrb",
|
|
959
|
+
"Roth 401k ctrb",
|
|
960
|
+
"IRA ctrb",
|
|
961
|
+
"Roth IRA ctrb",
|
|
962
|
+
"Roth conv",
|
|
963
|
+
"big-ticket items",
|
|
964
|
+
]
|
|
953
965
|
for i, iname in enumerate(self.inames):
|
|
954
966
|
h = self.horizons[i]
|
|
955
967
|
df = pd.DataFrame(0, index=np.arange(h), columns=cols)
|
|
956
|
-
df[
|
|
968
|
+
df["year"] = self.year_n[:h]
|
|
957
969
|
self.timeLists[iname] = df
|
|
958
970
|
|
|
959
|
-
self.caseStatus =
|
|
971
|
+
self.caseStatus = "modified"
|
|
960
972
|
|
|
961
973
|
return self.timeLists
|
|
962
974
|
|
|
@@ -977,7 +989,7 @@ class Plan(object):
|
|
|
977
989
|
is happening, and "w" is the width of the transition.
|
|
978
990
|
"""
|
|
979
991
|
c = self.interpCenter
|
|
980
|
-
w = self.interpWidth + 0.0001
|
|
992
|
+
w = self.interpWidth + 0.0001 # Avoid division by zero.
|
|
981
993
|
t = np.linspace(0, numPoints, numPoints)
|
|
982
994
|
# Solve 2x2 system to match end points exactly.
|
|
983
995
|
th0 = np.tanh((t[0] - c) / w)
|
|
@@ -997,10 +1009,10 @@ class Plan(object):
|
|
|
997
1009
|
Adjust parameters that follow inflation.
|
|
998
1010
|
"""
|
|
999
1011
|
if self.rateMethod is None:
|
|
1000
|
-
raise RuntimeError(
|
|
1012
|
+
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
1001
1013
|
|
|
1002
1014
|
if not self._adjustedParameters:
|
|
1003
|
-
self.mylog.vprint(
|
|
1015
|
+
self.mylog.vprint("Adjusting parameters for inflation.")
|
|
1004
1016
|
self.DeltaBar_tn = self.Delta_tn * self.gamma_n[:-1]
|
|
1005
1017
|
self.zetaBar_in = self.zeta_in * self.gamma_n[:-1]
|
|
1006
1018
|
self.sigmaBar_n = self.sigma_n * self.gamma_n[:-1]
|
|
@@ -1021,24 +1033,24 @@ class Plan(object):
|
|
|
1021
1033
|
"""
|
|
1022
1034
|
# Stack all variables in a single block vector.
|
|
1023
1035
|
C = {}
|
|
1024
|
-
C[
|
|
1025
|
-
C[
|
|
1026
|
-
C[
|
|
1027
|
-
C[
|
|
1028
|
-
C[
|
|
1029
|
-
C[
|
|
1030
|
-
C[
|
|
1031
|
-
C[
|
|
1032
|
-
C[
|
|
1033
|
-
self.nvars = _qC(C[
|
|
1036
|
+
C["b"] = 0
|
|
1037
|
+
C["d"] = _qC(C["b"], self.N_i, self.N_j, self.N_n + 1)
|
|
1038
|
+
C["e"] = _qC(C["d"], self.N_i, self.N_n)
|
|
1039
|
+
C["F"] = _qC(C["e"], self.N_n)
|
|
1040
|
+
C["g"] = _qC(C["F"], self.N_t, self.N_n)
|
|
1041
|
+
C["s"] = _qC(C["g"], self.N_n)
|
|
1042
|
+
C["w"] = _qC(C["s"], self.N_n)
|
|
1043
|
+
C["x"] = _qC(C["w"], self.N_i, self.N_j, self.N_n)
|
|
1044
|
+
C["z"] = _qC(C["x"], self.N_i, self.N_n)
|
|
1045
|
+
self.nvars = _qC(C["z"], self.N_i, self.N_n, self.N_z)
|
|
1034
1046
|
|
|
1035
1047
|
self.C = C
|
|
1036
1048
|
self.mylog.vprint(
|
|
1037
|
-
|
|
1049
|
+
"Problem has",
|
|
1038
1050
|
len(C),
|
|
1039
|
-
|
|
1051
|
+
"distinct time series forming",
|
|
1040
1052
|
self.nvars,
|
|
1041
|
-
|
|
1053
|
+
"decision variables.",
|
|
1042
1054
|
)
|
|
1043
1055
|
|
|
1044
1056
|
return None
|
|
@@ -1063,15 +1075,15 @@ class Plan(object):
|
|
|
1063
1075
|
i_s = self.i_s
|
|
1064
1076
|
n_d = self.n_d
|
|
1065
1077
|
|
|
1066
|
-
Cb = self.C[
|
|
1067
|
-
Cd = self.C[
|
|
1068
|
-
Ce = self.C[
|
|
1069
|
-
CF = self.C[
|
|
1070
|
-
Cg = self.C[
|
|
1071
|
-
Cs = self.C[
|
|
1072
|
-
Cw = self.C[
|
|
1073
|
-
Cx = self.C[
|
|
1074
|
-
Cz = self.C[
|
|
1078
|
+
Cb = self.C["b"]
|
|
1079
|
+
Cd = self.C["d"]
|
|
1080
|
+
Ce = self.C["e"]
|
|
1081
|
+
CF = self.C["F"]
|
|
1082
|
+
Cg = self.C["g"]
|
|
1083
|
+
Cs = self.C["s"]
|
|
1084
|
+
Cw = self.C["w"]
|
|
1085
|
+
Cx = self.C["x"]
|
|
1086
|
+
Cz = self.C["z"]
|
|
1075
1087
|
|
|
1076
1088
|
tau_ijn = np.zeros((Ni, Nj, Nn))
|
|
1077
1089
|
for i in range(Ni):
|
|
@@ -1083,15 +1095,15 @@ class Plan(object):
|
|
|
1083
1095
|
Tau1_ijn = 1 + tau_ijn
|
|
1084
1096
|
Tauh_ijn = 1 + tau_ijn / 2
|
|
1085
1097
|
|
|
1086
|
-
if
|
|
1087
|
-
units = u.getUnits(options[
|
|
1098
|
+
if "units" in options:
|
|
1099
|
+
units = u.getUnits(options["units"])
|
|
1088
1100
|
else:
|
|
1089
1101
|
units = 1000
|
|
1090
1102
|
|
|
1091
1103
|
bigM = 5e6
|
|
1092
|
-
if
|
|
1104
|
+
if "bigM" in options:
|
|
1093
1105
|
# No units for bigM.
|
|
1094
|
-
bigM = options[
|
|
1106
|
+
bigM = options["bigM"]
|
|
1095
1107
|
|
|
1096
1108
|
###################################################################
|
|
1097
1109
|
# Inequality constraint matrix with upper and lower bound vectors.
|
|
@@ -1118,16 +1130,16 @@ class Plan(object):
|
|
|
1118
1130
|
B.set0_Ub(_q1(Ce, n, Nn), self.sigmaBar_n[n])
|
|
1119
1131
|
|
|
1120
1132
|
# Roth conversions equalities/inequalities.
|
|
1121
|
-
if
|
|
1122
|
-
if options[
|
|
1133
|
+
if "maxRothConversion" in options:
|
|
1134
|
+
if options["maxRothConversion"] == "file":
|
|
1123
1135
|
# self.mylog.vprint('Fixing Roth conversions to those from file %s.' % self.timeListsFileName)
|
|
1124
1136
|
for i in range(Ni):
|
|
1125
1137
|
for n in range(self.horizons[i]):
|
|
1126
1138
|
rhs = self.myRothX_in[i][n]
|
|
1127
1139
|
B.setRange(_q2(Cx, i, n, Ni, Nn), rhs, rhs)
|
|
1128
1140
|
else:
|
|
1129
|
-
rhsopt = options[
|
|
1130
|
-
assert isinstance(rhsopt, (int, float)),
|
|
1141
|
+
rhsopt = options["maxRothConversion"]
|
|
1142
|
+
assert isinstance(rhsopt, (int, float)), "Specified maxConversion is not a number."
|
|
1131
1143
|
rhsopt *= units
|
|
1132
1144
|
if rhsopt < 0:
|
|
1133
1145
|
# self.mylog.vprint('Unlimited Roth conversions (<0)')
|
|
@@ -1140,12 +1152,12 @@ class Plan(object):
|
|
|
1140
1152
|
B.set0_Ub(_q2(Cx, i, n, Ni, Nn), rhsopt)
|
|
1141
1153
|
|
|
1142
1154
|
# Process noRothConversions option. Also valid when N_i == 1, why not?
|
|
1143
|
-
if
|
|
1144
|
-
rhsopt = options[
|
|
1155
|
+
if "noRothConversions" in options and options["noRothConversions"] != "None":
|
|
1156
|
+
rhsopt = options["noRothConversions"]
|
|
1145
1157
|
try:
|
|
1146
1158
|
i_x = self.inames.index(rhsopt)
|
|
1147
1159
|
except ValueError:
|
|
1148
|
-
raise ValueError(
|
|
1160
|
+
raise ValueError("Unknown individual %s for noRothConversions:" % rhsopt)
|
|
1149
1161
|
|
|
1150
1162
|
for n in range(Nn):
|
|
1151
1163
|
B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
|
|
@@ -1168,11 +1180,11 @@ class Plan(object):
|
|
|
1168
1180
|
A.addNewRow(rowDic, zero, inf)
|
|
1169
1181
|
|
|
1170
1182
|
# Constraints depending on objective function.
|
|
1171
|
-
if objective ==
|
|
1183
|
+
if objective == "maxSpending":
|
|
1172
1184
|
# Impose optional constraint on final bequest requested in today's $.
|
|
1173
|
-
if
|
|
1174
|
-
bequest = options[
|
|
1175
|
-
assert isinstance(bequest, (int, float)),
|
|
1185
|
+
if "bequest" in options:
|
|
1186
|
+
bequest = options["bequest"]
|
|
1187
|
+
assert isinstance(bequest, (int, float)), "Desired bequest is not a number."
|
|
1176
1188
|
bequest *= units * self.gamma_n[-1]
|
|
1177
1189
|
else:
|
|
1178
1190
|
# If not specified, defaults to $1 (nominal $).
|
|
@@ -1187,9 +1199,9 @@ class Plan(object):
|
|
|
1187
1199
|
row.addElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), 1)
|
|
1188
1200
|
A.addRow(row, bequest, bequest)
|
|
1189
1201
|
# self.mylog.vprint('Adding bequest constraint of:', u.d(bequest))
|
|
1190
|
-
elif objective ==
|
|
1191
|
-
spending = options[
|
|
1192
|
-
assert isinstance(spending, (int, float)),
|
|
1202
|
+
elif objective == "maxBequest":
|
|
1203
|
+
spending = options["netSpending"]
|
|
1204
|
+
assert isinstance(spending, (int, float)), "Desired spending provided is not a number."
|
|
1193
1205
|
# Account for time elapsed in the current year.
|
|
1194
1206
|
spending *= units * self.yearFracLeft
|
|
1195
1207
|
# self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
|
|
@@ -1362,15 +1374,15 @@ class Plan(object):
|
|
|
1362
1374
|
|
|
1363
1375
|
# Now build a solver-neutral objective vector.
|
|
1364
1376
|
c = abc.Objective(self.nvars)
|
|
1365
|
-
if objective ==
|
|
1377
|
+
if objective == "maxSpending":
|
|
1366
1378
|
c.setElem(_q1(Cg, 0, Nn), -1)
|
|
1367
|
-
elif objective ==
|
|
1379
|
+
elif objective == "maxBequest":
|
|
1368
1380
|
for i in range(Ni):
|
|
1369
1381
|
c.setElem(_q3(Cb, i, 0, Nn, Ni, Nj, Nn + 1), -1)
|
|
1370
1382
|
c.setElem(_q3(Cb, i, 1, Nn, Ni, Nj, Nn + 1), -(1 - self.nu))
|
|
1371
1383
|
c.setElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), -1)
|
|
1372
1384
|
else:
|
|
1373
|
-
raise RuntimeError(
|
|
1385
|
+
raise RuntimeError("Internal error in objective function.")
|
|
1374
1386
|
|
|
1375
1387
|
self.A = A
|
|
1376
1388
|
self.B = B
|
|
@@ -1385,19 +1397,19 @@ class Plan(object):
|
|
|
1385
1397
|
"""
|
|
1386
1398
|
if yend + self.N_n > self.year_n[0]:
|
|
1387
1399
|
yend = self.year_n[0] - self.N_n - 1
|
|
1388
|
-
self.mylog.vprint(
|
|
1400
|
+
self.mylog.vprint("Warning: Upper bound for year range re-adjusted to %d." % yend)
|
|
1389
1401
|
N = yend - ystart + 1
|
|
1390
1402
|
|
|
1391
|
-
self.mylog.vprint(
|
|
1403
|
+
self.mylog.vprint("Running historical range from %d to %d." % (ystart, yend))
|
|
1392
1404
|
|
|
1393
1405
|
self.mylog.setVerbose(verbose)
|
|
1394
1406
|
|
|
1395
|
-
if objective ==
|
|
1396
|
-
columns = [
|
|
1397
|
-
elif objective ==
|
|
1398
|
-
columns = [
|
|
1407
|
+
if objective == "maxSpending":
|
|
1408
|
+
columns = ["partial", objective]
|
|
1409
|
+
elif objective == "maxBequest":
|
|
1410
|
+
columns = ["partial", "final"]
|
|
1399
1411
|
else:
|
|
1400
|
-
self.mylog.print(
|
|
1412
|
+
self.mylog.print("Invalid objective %s." % objective)
|
|
1401
1413
|
return None
|
|
1402
1414
|
|
|
1403
1415
|
df = pd.DataFrame(columns=columns)
|
|
@@ -1409,14 +1421,14 @@ class Plan(object):
|
|
|
1409
1421
|
progcall.start()
|
|
1410
1422
|
|
|
1411
1423
|
for year in range(ystart, yend + 1):
|
|
1412
|
-
self.setRates(
|
|
1424
|
+
self.setRates("historical", year)
|
|
1413
1425
|
self.solve(objective, options)
|
|
1414
1426
|
if not verbose:
|
|
1415
|
-
progcall.show((year - ystart + 1)/N)
|
|
1416
|
-
if self.caseStatus ==
|
|
1417
|
-
if objective ==
|
|
1427
|
+
progcall.show((year - ystart + 1) / N)
|
|
1428
|
+
if self.caseStatus == "solved":
|
|
1429
|
+
if objective == "maxSpending":
|
|
1418
1430
|
df.loc[len(df)] = [self.partialBequest, self.basis]
|
|
1419
|
-
elif objective ==
|
|
1431
|
+
elif objective == "maxBequest":
|
|
1420
1432
|
df.loc[len(df)] = [self.partialBequest, self.bequest]
|
|
1421
1433
|
|
|
1422
1434
|
progcall.finish()
|
|
@@ -1434,26 +1446,26 @@ class Plan(object):
|
|
|
1434
1446
|
"""
|
|
1435
1447
|
Run Monte Carlo simulations on plan.
|
|
1436
1448
|
"""
|
|
1437
|
-
if self.rateMethod not in [
|
|
1438
|
-
self.mylog.print(
|
|
1449
|
+
if self.rateMethod not in ["stochastic", "histochastic"]:
|
|
1450
|
+
self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
|
|
1439
1451
|
return
|
|
1440
1452
|
|
|
1441
|
-
self.mylog.vprint(
|
|
1453
|
+
self.mylog.vprint("Running %d Monte Carlo simulations." % N)
|
|
1442
1454
|
self.mylog.setVerbose(verbose)
|
|
1443
1455
|
|
|
1444
1456
|
# Turn off Medicare by default, unless specified in options.
|
|
1445
|
-
if
|
|
1457
|
+
if "withMedicare" not in options:
|
|
1446
1458
|
myoptions = dict(options)
|
|
1447
|
-
myoptions[
|
|
1459
|
+
myoptions["withMedicare"] = False
|
|
1448
1460
|
else:
|
|
1449
1461
|
myoptions = options
|
|
1450
1462
|
|
|
1451
|
-
if objective ==
|
|
1452
|
-
columns = [
|
|
1453
|
-
elif objective ==
|
|
1454
|
-
columns = [
|
|
1463
|
+
if objective == "maxSpending":
|
|
1464
|
+
columns = ["partial", objective]
|
|
1465
|
+
elif objective == "maxBequest":
|
|
1466
|
+
columns = ["partial", "final"]
|
|
1455
1467
|
else:
|
|
1456
|
-
self.mylog.print(
|
|
1468
|
+
self.mylog.print("Invalid objective %s." % objective)
|
|
1457
1469
|
return None
|
|
1458
1470
|
|
|
1459
1471
|
df = pd.DataFrame(columns=columns)
|
|
@@ -1468,11 +1480,11 @@ class Plan(object):
|
|
|
1468
1480
|
self.regenRates()
|
|
1469
1481
|
self.solve(objective, myoptions)
|
|
1470
1482
|
if not verbose:
|
|
1471
|
-
progcall.show((n+1)/N)
|
|
1472
|
-
if self.caseStatus ==
|
|
1473
|
-
if objective ==
|
|
1483
|
+
progcall.show((n + 1) / N)
|
|
1484
|
+
if self.caseStatus == "solved":
|
|
1485
|
+
if objective == "maxSpending":
|
|
1474
1486
|
df.loc[len(df)] = [self.partialBequest, self.basis]
|
|
1475
|
-
elif objective ==
|
|
1487
|
+
elif objective == "maxBequest":
|
|
1476
1488
|
df.loc[len(df)] = [self.partialBequest, self.bequest]
|
|
1477
1489
|
|
|
1478
1490
|
progcall.finish()
|
|
@@ -1493,8 +1505,8 @@ class Plan(object):
|
|
|
1493
1505
|
|
|
1494
1506
|
description = io.StringIO()
|
|
1495
1507
|
|
|
1496
|
-
print(
|
|
1497
|
-
title =
|
|
1508
|
+
print("Success rate: %s on %d samples." % (u.pc(len(df) / N), N), file=description)
|
|
1509
|
+
title = "$N$ = %d, $P$ = %s" % (N, u.pc(len(df) / N))
|
|
1498
1510
|
means = df.mean(axis=0, numeric_only=True)
|
|
1499
1511
|
medians = df.median(axis=0, numeric_only=True)
|
|
1500
1512
|
|
|
@@ -1506,51 +1518,51 @@ class Plan(object):
|
|
|
1506
1518
|
# or if solution led to empty accounts at the end of first spouse's life.
|
|
1507
1519
|
if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
|
|
1508
1520
|
if medians.iloc[0] < 1:
|
|
1509
|
-
print(
|
|
1510
|
-
df.drop(
|
|
1521
|
+
print("Optimized solutions all have null partial bequest in year %d." % my[0], file=description)
|
|
1522
|
+
df.drop("partial", axis=1, inplace=True)
|
|
1511
1523
|
means = df.mean(axis=0, numeric_only=True)
|
|
1512
1524
|
medians = df.median(axis=0, numeric_only=True)
|
|
1513
1525
|
|
|
1514
1526
|
df /= 1000
|
|
1515
1527
|
if len(df) > 0:
|
|
1516
|
-
if objective ==
|
|
1528
|
+
if objective == "maxBequest":
|
|
1517
1529
|
fig, axes = plt.subplots()
|
|
1518
1530
|
# Show both partial and final bequests in the same histogram.
|
|
1519
|
-
sbn.histplot(df, multiple=
|
|
1531
|
+
sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
|
|
1520
1532
|
legend = []
|
|
1521
1533
|
# Don't know why but legend is reversed from df.
|
|
1522
1534
|
for q in range(len(means) - 1, -1, -1):
|
|
1523
1535
|
legend.append(
|
|
1524
|
-
|
|
1536
|
+
"%d: $M$: %s, $\\bar{x}$: %s"
|
|
1525
1537
|
% (my[q], u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True))
|
|
1526
1538
|
)
|
|
1527
1539
|
plt.legend(legend, shadow=True)
|
|
1528
|
-
plt.xlabel(
|
|
1540
|
+
plt.xlabel("%d $k" % self.year_n[0])
|
|
1529
1541
|
plt.title(objective)
|
|
1530
|
-
leads = [
|
|
1542
|
+
leads = ["partial %d" % my[0], " final %d" % my[1]]
|
|
1531
1543
|
elif len(means) == 2:
|
|
1532
1544
|
# Show partial bequest and net spending as two separate histograms.
|
|
1533
1545
|
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
1534
|
-
cols = [
|
|
1535
|
-
leads = [
|
|
1546
|
+
cols = ["partial", objective]
|
|
1547
|
+
leads = ["partial %d" % my[0], objective]
|
|
1536
1548
|
for q in range(2):
|
|
1537
1549
|
sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
|
|
1538
1550
|
legend = [
|
|
1539
|
-
(
|
|
1551
|
+
("$M$: %s, $\\bar{x}$: %s" % (u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True)))
|
|
1540
1552
|
]
|
|
1541
1553
|
axes[q].set_label(legend)
|
|
1542
1554
|
axes[q].legend(labels=legend)
|
|
1543
1555
|
axes[q].set_title(leads[q])
|
|
1544
|
-
axes[q].set_xlabel(
|
|
1556
|
+
axes[q].set_xlabel("%d $k" % self.year_n[0])
|
|
1545
1557
|
else:
|
|
1546
1558
|
# Show net spending as single histogram.
|
|
1547
1559
|
fig, axes = plt.subplots()
|
|
1548
1560
|
sbn.histplot(df[objective], kde=True, ax=axes)
|
|
1549
1561
|
legend = [
|
|
1550
|
-
(
|
|
1562
|
+
("$M$: %s, $\\bar{x}$: %s" % (u.d(medians.iloc[0], latex=True), u.d(means.iloc[0], latex=True)))
|
|
1551
1563
|
]
|
|
1552
1564
|
plt.legend(legend, shadow=True)
|
|
1553
|
-
plt.xlabel(
|
|
1565
|
+
plt.xlabel("%d $k" % self.year_n[0])
|
|
1554
1566
|
plt.title(objective)
|
|
1555
1567
|
leads = [objective]
|
|
1556
1568
|
|
|
@@ -1558,14 +1570,15 @@ class Plan(object):
|
|
|
1558
1570
|
# plt.show()
|
|
1559
1571
|
|
|
1560
1572
|
for q in range(len(means)):
|
|
1561
|
-
print(
|
|
1562
|
-
print(
|
|
1573
|
+
print("%12s: Median (%d $): %s" % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=description)
|
|
1574
|
+
print("%12s: Mean (%d $): %s" % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=description)
|
|
1563
1575
|
print(
|
|
1564
|
-
|
|
1576
|
+
"%12s: Range: %s - %s"
|
|
1565
1577
|
% (leads[q], u.d(1000 * df.iloc[:, q].min()), u.d(1000 * df.iloc[:, q].max())),
|
|
1566
|
-
file=description
|
|
1578
|
+
file=description,
|
|
1579
|
+
)
|
|
1567
1580
|
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
1568
|
-
print(
|
|
1581
|
+
print("%12s: N zero solns: %d" % (leads[q], nzeros), file=description)
|
|
1569
1582
|
|
|
1570
1583
|
return fig, description
|
|
1571
1584
|
|
|
@@ -1597,24 +1610,24 @@ class Plan(object):
|
|
|
1597
1610
|
Refer to companion document for implementation details.
|
|
1598
1611
|
"""
|
|
1599
1612
|
if self.rateMethod is None:
|
|
1600
|
-
raise RuntimeError(
|
|
1613
|
+
raise RuntimeError("Rate method must be selected before solving.")
|
|
1601
1614
|
|
|
1602
1615
|
# Assume unsuccessful until problem solved.
|
|
1603
|
-
self.caseStatus =
|
|
1616
|
+
self.caseStatus = "unsuccessful"
|
|
1604
1617
|
|
|
1605
1618
|
# Check objective and required options.
|
|
1606
|
-
knownObjectives = [
|
|
1607
|
-
knownSolvers = [
|
|
1619
|
+
knownObjectives = ["maxBequest", "maxSpending"]
|
|
1620
|
+
knownSolvers = ["HiGHS", "MOSEK"]
|
|
1608
1621
|
knownOptions = [
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1622
|
+
"units",
|
|
1623
|
+
"maxRothConversion",
|
|
1624
|
+
"netSpending",
|
|
1625
|
+
"bequest",
|
|
1626
|
+
"bigM",
|
|
1627
|
+
"noRothConversions",
|
|
1628
|
+
"withMedicare",
|
|
1629
|
+
"solver",
|
|
1630
|
+
"previousMAGIs",
|
|
1618
1631
|
]
|
|
1619
1632
|
# We will modify options if required.
|
|
1620
1633
|
if options is None:
|
|
@@ -1624,48 +1637,48 @@ class Plan(object):
|
|
|
1624
1637
|
|
|
1625
1638
|
for opt in myoptions:
|
|
1626
1639
|
if opt not in knownOptions:
|
|
1627
|
-
raise ValueError(
|
|
1640
|
+
raise ValueError("Option %s is not one of %r." % (opt, knownOptions))
|
|
1628
1641
|
|
|
1629
1642
|
if objective not in knownObjectives:
|
|
1630
|
-
raise ValueError(
|
|
1643
|
+
raise ValueError("Objective %s is not one of %r." % (objective, knownObjectives))
|
|
1631
1644
|
|
|
1632
|
-
if objective ==
|
|
1633
|
-
raise RuntimeError(
|
|
1645
|
+
if objective == "maxBequest" and "netSpending" not in myoptions:
|
|
1646
|
+
raise RuntimeError("Objective %s needs netSpending option." % objective)
|
|
1634
1647
|
|
|
1635
|
-
if objective ==
|
|
1636
|
-
self.mylog.vprint(
|
|
1637
|
-
myoptions.pop(
|
|
1648
|
+
if objective == "maxBequest" and "bequest" in myoptions:
|
|
1649
|
+
self.mylog.vprint("Ignoring bequest option provided.")
|
|
1650
|
+
myoptions.pop("bequest")
|
|
1638
1651
|
|
|
1639
|
-
if objective ==
|
|
1640
|
-
self.mylog.vprint(
|
|
1641
|
-
myoptions.pop(
|
|
1652
|
+
if objective == "maxSpending" and "netSpending" in myoptions:
|
|
1653
|
+
self.mylog.vprint("Ignoring netSpending option provided.")
|
|
1654
|
+
myoptions.pop("netSpending")
|
|
1642
1655
|
|
|
1643
|
-
if objective ==
|
|
1644
|
-
self.mylog.vprint(
|
|
1656
|
+
if objective == "maxSpending" and "bequest" not in myoptions:
|
|
1657
|
+
self.mylog.vprint("Using bequest of $1.")
|
|
1645
1658
|
|
|
1646
|
-
if
|
|
1647
|
-
magi = myoptions[
|
|
1659
|
+
if "previousMAGIs" in myoptions:
|
|
1660
|
+
magi = myoptions["previousMAGIs"]
|
|
1648
1661
|
if len(magi) != 2:
|
|
1649
1662
|
raise ValueError("previousMAGIs must have two values.")
|
|
1650
1663
|
|
|
1651
|
-
if
|
|
1652
|
-
units = u.getUnits(options[
|
|
1664
|
+
if "units" in options:
|
|
1665
|
+
units = u.getUnits(options["units"])
|
|
1653
1666
|
else:
|
|
1654
1667
|
units = 1000
|
|
1655
1668
|
self.prevMAGI = units * np.array(magi)
|
|
1656
1669
|
|
|
1657
1670
|
self._adjustParameters()
|
|
1658
1671
|
|
|
1659
|
-
if
|
|
1660
|
-
solver = myoptions[
|
|
1672
|
+
if "solver" in options:
|
|
1673
|
+
solver = myoptions["solver"]
|
|
1661
1674
|
if solver not in knownSolvers:
|
|
1662
|
-
raise ValueError(
|
|
1675
|
+
raise ValueError("Unknown solver %s." % solver)
|
|
1663
1676
|
else:
|
|
1664
1677
|
solver = self.defaultSolver
|
|
1665
1678
|
|
|
1666
|
-
if solver ==
|
|
1679
|
+
if solver == "HiGHS":
|
|
1667
1680
|
self._milpSolve(objective, myoptions)
|
|
1668
|
-
elif solver ==
|
|
1681
|
+
elif solver == "MOSEK":
|
|
1669
1682
|
self._mosekSolve(objective, myoptions)
|
|
1670
1683
|
|
|
1671
1684
|
self.objective = objective
|
|
@@ -1680,16 +1693,16 @@ class Plan(object):
|
|
|
1680
1693
|
from scipy import optimize
|
|
1681
1694
|
|
|
1682
1695
|
withMedicare = True
|
|
1683
|
-
if
|
|
1696
|
+
if "withMedicare" in options and options["withMedicare"] is False:
|
|
1684
1697
|
withMedicare = False
|
|
1685
1698
|
|
|
1686
|
-
if objective ==
|
|
1699
|
+
if objective == "maxSpending":
|
|
1687
1700
|
objFac = -1 / self.xi_n[0]
|
|
1688
1701
|
else:
|
|
1689
1702
|
objFac = -1 / self.gamma_n[-1]
|
|
1690
1703
|
|
|
1691
1704
|
# mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
|
|
1692
|
-
milpOptions = {
|
|
1705
|
+
milpOptions = {"disp": False, "mip_rel_gap": 1e-6}
|
|
1693
1706
|
|
|
1694
1707
|
it = 0
|
|
1695
1708
|
absdiff = np.inf
|
|
@@ -1722,38 +1735,38 @@ class Plan(object):
|
|
|
1722
1735
|
|
|
1723
1736
|
self._estimateMedicare(solution.x)
|
|
1724
1737
|
|
|
1725
|
-
self.mylog.vprint(
|
|
1738
|
+
self.mylog.vprint("Iteration:", it, "objective:", u.d(solution.fun * objFac, f=2))
|
|
1726
1739
|
|
|
1727
1740
|
delta = solution.x - old_x
|
|
1728
1741
|
absdiff = np.sum(np.abs(delta), axis=0)
|
|
1729
1742
|
if absdiff < 1:
|
|
1730
|
-
self.mylog.vprint(
|
|
1743
|
+
self.mylog.vprint("Converged on full solution.")
|
|
1731
1744
|
break
|
|
1732
1745
|
|
|
1733
1746
|
# Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
|
|
1734
|
-
isclosenough = abs(-solution.fun - min(old_solutions[int(it / 2):])) < 10 * self.xi_n[0]
|
|
1747
|
+
isclosenough = abs(-solution.fun - min(old_solutions[int(it / 2) :])) < 10 * self.xi_n[0]
|
|
1735
1748
|
if isclosenough:
|
|
1736
|
-
self.mylog.vprint(
|
|
1749
|
+
self.mylog.vprint("Converged through selecting minimum oscillating objective.")
|
|
1737
1750
|
break
|
|
1738
1751
|
|
|
1739
1752
|
if it > 59:
|
|
1740
|
-
self.mylog.vprint(
|
|
1753
|
+
self.mylog.vprint("WARNING: Exiting loop on maximum iterations.")
|
|
1741
1754
|
break
|
|
1742
1755
|
|
|
1743
1756
|
old_solutions.append(-solution.fun)
|
|
1744
1757
|
old_x = solution.x
|
|
1745
1758
|
|
|
1746
1759
|
if solution.success:
|
|
1747
|
-
self.mylog.vprint(
|
|
1760
|
+
self.mylog.vprint("Self-consistent Medicare loop returned after %d iterations." % it)
|
|
1748
1761
|
self.mylog.vprint(solution.message)
|
|
1749
|
-
self.mylog.vprint(
|
|
1762
|
+
self.mylog.vprint("Objective:", u.d(solution.fun * objFac))
|
|
1750
1763
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1751
1764
|
self._aggregateResults(solution.x)
|
|
1752
|
-
self._timestamp = datetime.now().strftime(
|
|
1753
|
-
self.caseStatus =
|
|
1765
|
+
self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
|
|
1766
|
+
self.caseStatus = "solved"
|
|
1754
1767
|
else:
|
|
1755
|
-
self.mylog.vprint(
|
|
1756
|
-
self.caseStatus =
|
|
1768
|
+
self.mylog.vprint("WARNING: Optimization failed:", solution.message, solution.success)
|
|
1769
|
+
self.caseStatus = "unsuccessful"
|
|
1757
1770
|
|
|
1758
1771
|
return None
|
|
1759
1772
|
|
|
@@ -1764,10 +1777,10 @@ class Plan(object):
|
|
|
1764
1777
|
import mosek
|
|
1765
1778
|
|
|
1766
1779
|
withMedicare = True
|
|
1767
|
-
if
|
|
1780
|
+
if "withMedicare" in options and options["withMedicare"] is False:
|
|
1768
1781
|
withMedicare = False
|
|
1769
1782
|
|
|
1770
|
-
if objective ==
|
|
1783
|
+
if objective == "maxSpending":
|
|
1771
1784
|
objFac = -1 / self.xi_n[0]
|
|
1772
1785
|
else:
|
|
1773
1786
|
objFac = -1 / self.gamma_n[-1]
|
|
@@ -1775,11 +1788,11 @@ class Plan(object):
|
|
|
1775
1788
|
# mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
|
|
1776
1789
|
|
|
1777
1790
|
bdic = {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1791
|
+
"fx": mosek.boundkey.fx,
|
|
1792
|
+
"fr": mosek.boundkey.fr,
|
|
1793
|
+
"lo": mosek.boundkey.lo,
|
|
1794
|
+
"ra": mosek.boundkey.ra,
|
|
1795
|
+
"up": mosek.boundkey.up,
|
|
1783
1796
|
}
|
|
1784
1797
|
|
|
1785
1798
|
it = 0
|
|
@@ -1834,22 +1847,22 @@ class Plan(object):
|
|
|
1834
1847
|
|
|
1835
1848
|
self._estimateMedicare(xx)
|
|
1836
1849
|
|
|
1837
|
-
self.mylog.vprint(
|
|
1850
|
+
self.mylog.vprint("Iteration:", it, "objective:", u.d(solution * objFac, f=2))
|
|
1838
1851
|
|
|
1839
1852
|
delta = xx - old_x
|
|
1840
1853
|
absdiff = np.sum(np.abs(delta), axis=0)
|
|
1841
1854
|
if absdiff < 1:
|
|
1842
|
-
self.mylog.vprint(
|
|
1855
|
+
self.mylog.vprint("Converged on full solution.")
|
|
1843
1856
|
break
|
|
1844
1857
|
|
|
1845
1858
|
# Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
|
|
1846
|
-
isclosenough = abs(-solution - min(old_solutions[int(it / 2):])) < 10 * self.xi_n[0]
|
|
1859
|
+
isclosenough = abs(-solution - min(old_solutions[int(it / 2) :])) < 10 * self.xi_n[0]
|
|
1847
1860
|
if isclosenough:
|
|
1848
|
-
self.mylog.vprint(
|
|
1861
|
+
self.mylog.vprint("Converged through selecting minimum oscillating objective.")
|
|
1849
1862
|
break
|
|
1850
1863
|
|
|
1851
1864
|
if it > 59:
|
|
1852
|
-
self.mylog.vprint(
|
|
1865
|
+
self.mylog.vprint("WARNING: Exiting loop on maximum iterations.")
|
|
1853
1866
|
break
|
|
1854
1867
|
|
|
1855
1868
|
old_solutions.append(-solution)
|
|
@@ -1858,17 +1871,17 @@ class Plan(object):
|
|
|
1858
1871
|
task.set_Stream(mosek.streamtype.wrn, _streamPrinter)
|
|
1859
1872
|
# task.writedata(self._name+'.ptf')
|
|
1860
1873
|
if solsta == mosek.solsta.integer_optimal:
|
|
1861
|
-
self.mylog.vprint(
|
|
1874
|
+
self.mylog.vprint("Self-consistent Medicare loop returned after %d iterations." % it)
|
|
1862
1875
|
task.solutionsummary(mosek.streamtype.msg)
|
|
1863
|
-
self.mylog.vprint(
|
|
1864
|
-
self.caseStatus =
|
|
1876
|
+
self.mylog.vprint("Objective:", u.d(solution * objFac))
|
|
1877
|
+
self.caseStatus = "solved"
|
|
1865
1878
|
# self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
|
|
1866
1879
|
self._aggregateResults(xx)
|
|
1867
|
-
self._timestamp = datetime.now().strftime(
|
|
1880
|
+
self._timestamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
1868
1881
|
else:
|
|
1869
|
-
self.mylog.vprint(
|
|
1882
|
+
self.mylog.vprint("WARNING: Optimization failed:", "Infeasible or unbounded.")
|
|
1870
1883
|
task.solutionsummary(mosek.streamtype.msg)
|
|
1871
|
-
self.caseStatus =
|
|
1884
|
+
self.caseStatus = "unsuccessful"
|
|
1872
1885
|
|
|
1873
1886
|
return None
|
|
1874
1887
|
|
|
@@ -1883,9 +1896,9 @@ class Plan(object):
|
|
|
1883
1896
|
if x is None:
|
|
1884
1897
|
MAGI_n = np.zeros(self.N_n)
|
|
1885
1898
|
else:
|
|
1886
|
-
self.F_tn = np.array(x[self.C[
|
|
1899
|
+
self.F_tn = np.array(x[self.C["F"] : self.C["g"]])
|
|
1887
1900
|
self.F_tn = self.F_tn.reshape((self.N_t, self.N_n))
|
|
1888
|
-
MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C[
|
|
1901
|
+
MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C["e"] : self.C["F"]])
|
|
1889
1902
|
|
|
1890
1903
|
self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1891
1904
|
|
|
@@ -1905,15 +1918,15 @@ class Plan(object):
|
|
|
1905
1918
|
# Nz = self.N_z
|
|
1906
1919
|
n_d = self.n_d
|
|
1907
1920
|
|
|
1908
|
-
Cb = self.C[
|
|
1909
|
-
Cd = self.C[
|
|
1910
|
-
Ce = self.C[
|
|
1911
|
-
CF = self.C[
|
|
1912
|
-
Cg = self.C[
|
|
1913
|
-
Cs = self.C[
|
|
1914
|
-
Cw = self.C[
|
|
1915
|
-
Cx = self.C[
|
|
1916
|
-
Cz = self.C[
|
|
1921
|
+
Cb = self.C["b"]
|
|
1922
|
+
Cd = self.C["d"]
|
|
1923
|
+
Ce = self.C["e"]
|
|
1924
|
+
CF = self.C["F"]
|
|
1925
|
+
Cg = self.C["g"]
|
|
1926
|
+
Cs = self.C["s"]
|
|
1927
|
+
Cw = self.C["w"]
|
|
1928
|
+
Cx = self.C["x"]
|
|
1929
|
+
Cz = self.C["z"]
|
|
1917
1930
|
|
|
1918
1931
|
x = u.roundCents(x)
|
|
1919
1932
|
|
|
@@ -2006,20 +2019,20 @@ class Plan(object):
|
|
|
2006
2019
|
]
|
|
2007
2020
|
"""
|
|
2008
2021
|
sources = {}
|
|
2009
|
-
sources[
|
|
2010
|
-
sources[
|
|
2011
|
-
sources[
|
|
2012
|
-
sources[
|
|
2013
|
-
sources[
|
|
2014
|
-
sources[
|
|
2015
|
-
sources[
|
|
2016
|
-
sources[
|
|
2017
|
-
sources[
|
|
2022
|
+
sources["wages"] = self.omega_in
|
|
2023
|
+
sources["ssec"] = self.zetaBar_in
|
|
2024
|
+
sources["pension"] = self.piBar_in
|
|
2025
|
+
sources["txbl acc wdrwl"] = self.w_ijn[:, 0, :]
|
|
2026
|
+
sources["RMD"] = self.rmd_in
|
|
2027
|
+
sources["+dist"] = self.dist_in
|
|
2028
|
+
sources["RothX"] = self.x_in
|
|
2029
|
+
sources["tax-free wdrwl"] = self.w_ijn[:, 2, :]
|
|
2030
|
+
sources["BTI"] = self.Lambda_in
|
|
2018
2031
|
|
|
2019
2032
|
savings = {}
|
|
2020
|
-
savings[
|
|
2021
|
-
savings[
|
|
2022
|
-
savings[
|
|
2033
|
+
savings["taxable"] = self.b_ijn[:, 0, :]
|
|
2034
|
+
savings["tax-deferred"] = self.b_ijn[:, 1, :]
|
|
2035
|
+
savings["tax-free"] = self.b_ijn[:, 2, :]
|
|
2023
2036
|
|
|
2024
2037
|
self.sources_in = sources
|
|
2025
2038
|
self.savings_in = savings
|
|
@@ -2039,7 +2052,7 @@ class Plan(object):
|
|
|
2039
2052
|
"""
|
|
2040
2053
|
_estate = np.sum(self.b_ijn[:, :, :, self.N_n], axis=(0, 2))
|
|
2041
2054
|
_estate[1] *= 1 - self.nu
|
|
2042
|
-
self.mylog.vprint(
|
|
2055
|
+
self.mylog.vprint("Estate value of %s at the end of year %s." % (u.d(sum(_estate)), self.year_n[-1]))
|
|
2043
2056
|
|
|
2044
2057
|
return None
|
|
2045
2058
|
|
|
@@ -2048,11 +2061,11 @@ class Plan(object):
|
|
|
2048
2061
|
"""
|
|
2049
2062
|
Print summary in logs.
|
|
2050
2063
|
"""
|
|
2051
|
-
self.mylog.print(
|
|
2064
|
+
self.mylog.print("SUMMARY ================================================================")
|
|
2052
2065
|
dic = self.summaryDic()
|
|
2053
2066
|
for key, value in dic.items():
|
|
2054
2067
|
self.mylog.print(f"{key}: {value}")
|
|
2055
|
-
self.mylog.print(
|
|
2068
|
+
self.mylog.print("------------------------------------------------------------------------")
|
|
2056
2069
|
|
|
2057
2070
|
return None
|
|
2058
2071
|
|
|
@@ -2071,11 +2084,10 @@ class Plan(object):
|
|
|
2071
2084
|
"""
|
|
2072
2085
|
Return summary as a string.
|
|
2073
2086
|
"""
|
|
2074
|
-
string =
|
|
2087
|
+
string = "Synopsis\n"
|
|
2075
2088
|
dic = self.summaryDic()
|
|
2076
2089
|
for key, value in dic.items():
|
|
2077
2090
|
string += f"{key:>70}: {value}\n"
|
|
2078
|
-
# string += "%60s: %s\n" % (key, value)
|
|
2079
2091
|
|
|
2080
2092
|
return string
|
|
2081
2093
|
|
|
@@ -2086,39 +2098,29 @@ class Plan(object):
|
|
|
2086
2098
|
now = self.year_n[0]
|
|
2087
2099
|
dic = {}
|
|
2088
2100
|
# Results
|
|
2089
|
-
dic[f"Net yearly spending basis in {now}$"] =
|
|
2090
|
-
dic[f"Net yearly spending for year {now}"] =
|
|
2101
|
+
dic[f"Net yearly spending basis in {now}$"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2102
|
+
dic[f"Net yearly spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
|
|
2091
2103
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2092
2104
|
|
|
2093
2105
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2094
2106
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2095
|
-
dic[f"Total net spending in {now}$"] = (
|
|
2096
|
-
"%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
|
|
2097
|
-
)
|
|
2107
|
+
dic[f"Total net spending in {now}$"] = "%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
|
|
2098
2108
|
|
|
2099
2109
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2100
2110
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2101
|
-
dic[f"Total Roth conversions in {now}$"] = (
|
|
2102
|
-
"%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
|
|
2103
|
-
)
|
|
2111
|
+
dic[f"Total Roth conversions in {now}$"] = "%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
|
|
2104
2112
|
|
|
2105
2113
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2106
2114
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2107
|
-
dic[f"Total income tax paid on ordinary income in {now}$"] = (
|
|
2108
|
-
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2109
|
-
)
|
|
2115
|
+
dic[f"Total income tax paid on ordinary income in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2110
2116
|
|
|
2111
2117
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2112
2118
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2113
|
-
dic[f"Total tax paid on gains and dividends in {now}$"] = (
|
|
2114
|
-
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2115
|
-
)
|
|
2119
|
+
dic[f"Total tax paid on gains and dividends in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2116
2120
|
|
|
2117
2121
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2118
2122
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2119
|
-
dic[f"Total Medicare premiums paid in {now}$"] = (
|
|
2120
|
-
"%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2121
|
-
)
|
|
2123
|
+
dic[f"Total Medicare premiums paid in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
|
|
2122
2124
|
|
|
2123
2125
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2124
2126
|
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
@@ -2129,23 +2131,21 @@ class Plan(object):
|
|
|
2129
2131
|
q_j = self.partialEstate_j * self.phi_j
|
|
2130
2132
|
totSpousal = np.sum(q_j)
|
|
2131
2133
|
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2132
|
-
dic[
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
)
|
|
2134
|
+
dic[
|
|
2135
|
+
"Spousal wealth transfer from %s to %s in year %d (nominal)"
|
|
2136
|
+
% (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx])
|
|
2137
|
+
] = "taxable: %s tax-def: %s tax-free: %s" % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2]))
|
|
2136
2138
|
|
|
2137
|
-
dic["Sum of spousal bequests to %s in year %d in %d$" %
|
|
2138
|
-
(self.inames[self.i_s], self.year_n[nx], now)] = (
|
|
2139
|
+
dic["Sum of spousal bequests to %s in year %d in %d$" % (self.inames[self.i_s], self.year_n[nx], now)] = (
|
|
2139
2140
|
"%s (%s nominal)" % (u.d(totSpousalNow), u.d(totSpousal))
|
|
2140
2141
|
)
|
|
2141
|
-
dic[
|
|
2142
|
-
(self.inames[self.i_d], self.year_n[nx])
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
(self.inames[self.i_d], self.year_n[nx], now)
|
|
2147
|
-
|
|
2148
|
-
)
|
|
2142
|
+
dic[
|
|
2143
|
+
"Post-tax non-spousal bequests from %s in year %d (nominal)" % (self.inames[self.i_d], self.year_n[nx])
|
|
2144
|
+
] = "taxable: %s tax-def: %s tax-free: %s" % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2]))
|
|
2145
|
+
dic[
|
|
2146
|
+
"Sum of post-tax non-spousal bequests from %s in year %d in %d$"
|
|
2147
|
+
% (self.inames[self.i_d], self.year_n[nx], now)
|
|
2148
|
+
] = "%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
|
|
2149
2149
|
|
|
2150
2150
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2151
2151
|
estate[1] *= 1 - self.nu
|
|
@@ -2159,12 +2159,11 @@ class Plan(object):
|
|
|
2159
2159
|
"%s (%s nominal)" % (u.d(totEstateNow), u.d(totEstate))
|
|
2160
2160
|
)
|
|
2161
2161
|
dic["Plan starting date"] = str(self.startDate)
|
|
2162
|
-
dic["Cumulative inflation factor from start date to end of plan"] = (
|
|
2163
|
-
"%.2f" % (self.gamma_n[-1])
|
|
2164
|
-
)
|
|
2162
|
+
dic["Cumulative inflation factor from start date to end of plan"] = "%.2f" % (self.gamma_n[-1])
|
|
2165
2163
|
for i in range(self.N_i):
|
|
2166
|
-
dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = (
|
|
2167
|
-
|
|
2164
|
+
dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = "%d -> %d" % (
|
|
2165
|
+
now,
|
|
2166
|
+
now + self.horizons[i] - 1,
|
|
2168
2167
|
)
|
|
2169
2168
|
|
|
2170
2169
|
dic["Plan name"] = self._name
|
|
@@ -2174,7 +2173,7 @@ class Plan(object):
|
|
|
2174
2173
|
|
|
2175
2174
|
return dic
|
|
2176
2175
|
|
|
2177
|
-
def showRatesCorrelations(self, tag=
|
|
2176
|
+
def showRatesCorrelations(self, tag="", shareRange=False, figure=False):
|
|
2178
2177
|
"""
|
|
2179
2178
|
Plot correlations between various rates.
|
|
2180
2179
|
|
|
@@ -2182,15 +2181,15 @@ class Plan(object):
|
|
|
2182
2181
|
"""
|
|
2183
2182
|
import seaborn as sbn
|
|
2184
2183
|
|
|
2185
|
-
if self.rateMethod in [None,
|
|
2186
|
-
self.mylog.vprint(
|
|
2184
|
+
if self.rateMethod in [None, "user", "historical average", "conservative"]:
|
|
2185
|
+
self.mylog.vprint("Warning: Cannot plot correlations for %s rate method." % self.rateMethod)
|
|
2187
2186
|
return None
|
|
2188
2187
|
|
|
2189
2188
|
rateNames = [
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2189
|
+
"S&P500 (incl. div.)",
|
|
2190
|
+
"Baa Corp. Bonds",
|
|
2191
|
+
"10-y T-Notes",
|
|
2192
|
+
"Inflation",
|
|
2194
2193
|
]
|
|
2195
2194
|
|
|
2196
2195
|
df = pd.DataFrame()
|
|
@@ -2206,25 +2205,25 @@ class Plan(object):
|
|
|
2206
2205
|
g.map_upper(sbn.scatterplot)
|
|
2207
2206
|
g.map_lower(sbn.kdeplot)
|
|
2208
2207
|
# g.map_diag(sbn.kdeplot)
|
|
2209
|
-
g.map_diag(sbn.histplot, color=
|
|
2208
|
+
g.map_diag(sbn.histplot, color="orange")
|
|
2210
2209
|
|
|
2211
2210
|
# Put zero axes on off-diagonal plots.
|
|
2212
2211
|
imod = len(rateNames) + 1
|
|
2213
2212
|
for i, ax in enumerate(g.axes.flat):
|
|
2214
|
-
ax.axvline(x=0, color=
|
|
2213
|
+
ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
|
|
2215
2214
|
if i % imod != 0:
|
|
2216
|
-
ax.axhline(y=0, color=
|
|
2215
|
+
ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
|
|
2217
2216
|
# ax.tick_params(axis='both', labelleft=True, labelbottom=True)
|
|
2218
2217
|
|
|
2219
2218
|
# plt.subplots_adjust(wspace=0.3, hspace=0.3)
|
|
2220
2219
|
|
|
2221
|
-
title = self._name +
|
|
2222
|
-
title +=
|
|
2223
|
-
if self.rateMethod in [
|
|
2224
|
-
title +=
|
|
2220
|
+
title = self._name + "\n"
|
|
2221
|
+
title += "Rates Correlations (N=%d) %s" % (self.N_n, self.rateMethod)
|
|
2222
|
+
if self.rateMethod in ["historical", "histochastic"]:
|
|
2223
|
+
title += " (" + str(self.rateFrm) + "-" + str(self.rateTo) + ")"
|
|
2225
2224
|
|
|
2226
|
-
if tag !=
|
|
2227
|
-
title +=
|
|
2225
|
+
if tag != "":
|
|
2226
|
+
title += " - " + tag
|
|
2228
2227
|
|
|
2229
2228
|
g.fig.suptitle(title, y=1.08)
|
|
2230
2229
|
|
|
@@ -2234,7 +2233,7 @@ class Plan(object):
|
|
|
2234
2233
|
plt.show()
|
|
2235
2234
|
return None
|
|
2236
2235
|
|
|
2237
|
-
def showRates(self, tag=
|
|
2236
|
+
def showRates(self, tag="", figure=False):
|
|
2238
2237
|
"""
|
|
2239
2238
|
Plot rate values used over the time horizon.
|
|
2240
2239
|
|
|
@@ -2243,26 +2242,26 @@ class Plan(object):
|
|
|
2243
2242
|
import matplotlib.ticker as tk
|
|
2244
2243
|
|
|
2245
2244
|
if self.rateMethod is None:
|
|
2246
|
-
self.mylog.vprint(
|
|
2245
|
+
self.mylog.vprint("Warning: Rate method must be selected before plotting.")
|
|
2247
2246
|
return None
|
|
2248
2247
|
|
|
2249
2248
|
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2250
|
-
plt.grid(visible=
|
|
2251
|
-
title = self._name +
|
|
2252
|
-
if self.rateMethod in [
|
|
2253
|
-
title +=
|
|
2254
|
-
title +=
|
|
2249
|
+
plt.grid(visible="both")
|
|
2250
|
+
title = self._name + "\nReturn & Inflation Rates (" + str(self.rateMethod)
|
|
2251
|
+
if self.rateMethod in ["historical", "histochastic", "historical average"]:
|
|
2252
|
+
title += " " + str(self.rateFrm) + "-" + str(self.rateTo)
|
|
2253
|
+
title += ")"
|
|
2255
2254
|
|
|
2256
|
-
if tag !=
|
|
2257
|
-
title +=
|
|
2255
|
+
if tag != "":
|
|
2256
|
+
title += " - " + tag
|
|
2258
2257
|
|
|
2259
2258
|
rateName = [
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2259
|
+
"S&P500 (incl. div.)",
|
|
2260
|
+
"Baa Corp. Bonds",
|
|
2261
|
+
"10-y T-Notes",
|
|
2262
|
+
"Inflation",
|
|
2264
2263
|
]
|
|
2265
|
-
ltype = [
|
|
2264
|
+
ltype = ["-", "-.", ":", "--"]
|
|
2266
2265
|
for k in range(self.N_k):
|
|
2267
2266
|
if self.yearFracLeft == 1:
|
|
2268
2267
|
data = 100 * self.tau_kn[k]
|
|
@@ -2272,16 +2271,17 @@ class Plan(object):
|
|
|
2272
2271
|
years = self.year_n[1:]
|
|
2273
2272
|
|
|
2274
2273
|
# Use ddof=1 to match pandas.
|
|
2275
|
-
label = (
|
|
2276
|
-
|
|
2274
|
+
label = (
|
|
2275
|
+
rateName[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
|
|
2276
|
+
)
|
|
2277
2277
|
ax.plot(years, data, label=label, ls=ltype[k % self.N_k])
|
|
2278
2278
|
|
|
2279
2279
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2280
|
-
ax.legend(loc=
|
|
2280
|
+
ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
|
|
2281
2281
|
# ax.legend(loc='upper left')
|
|
2282
2282
|
ax.set_title(title)
|
|
2283
|
-
ax.set_xlabel(
|
|
2284
|
-
ax.set_ylabel(
|
|
2283
|
+
ax.set_xlabel("year")
|
|
2284
|
+
ax.set_ylabel("%")
|
|
2285
2285
|
|
|
2286
2286
|
if figure:
|
|
2287
2287
|
return fig
|
|
@@ -2289,24 +2289,24 @@ class Plan(object):
|
|
|
2289
2289
|
plt.show()
|
|
2290
2290
|
return None
|
|
2291
2291
|
|
|
2292
|
-
def showProfile(self, tag=
|
|
2292
|
+
def showProfile(self, tag="", figure=False):
|
|
2293
2293
|
"""
|
|
2294
2294
|
Plot spending profile over time.
|
|
2295
2295
|
|
|
2296
2296
|
A tag string can be set to add information to the title of the plot.
|
|
2297
2297
|
"""
|
|
2298
2298
|
if self.xi_n is None:
|
|
2299
|
-
self.mylog.vprint(
|
|
2299
|
+
self.mylog.vprint("Warning: Profile must be selected before plotting.")
|
|
2300
2300
|
return None
|
|
2301
2301
|
|
|
2302
|
-
title = self._name +
|
|
2303
|
-
if tag !=
|
|
2304
|
-
title +=
|
|
2302
|
+
title = self._name + "\nSpending Profile"
|
|
2303
|
+
if tag != "":
|
|
2304
|
+
title += " - " + tag
|
|
2305
2305
|
|
|
2306
2306
|
# style = {'net': '-', 'target': ':'}
|
|
2307
|
-
style = {
|
|
2308
|
-
series = {
|
|
2309
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat=
|
|
2307
|
+
style = {"profile": "-"}
|
|
2308
|
+
series = {"profile": self.xi_n}
|
|
2309
|
+
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="xi")
|
|
2310
2310
|
|
|
2311
2311
|
if figure:
|
|
2312
2312
|
return fig
|
|
@@ -2315,7 +2315,7 @@ class Plan(object):
|
|
|
2315
2315
|
return None
|
|
2316
2316
|
|
|
2317
2317
|
@_checkCaseStatus
|
|
2318
|
-
def showNetSpending(self, tag=
|
|
2318
|
+
def showNetSpending(self, tag="", value=None, figure=False):
|
|
2319
2319
|
"""
|
|
2320
2320
|
Plot net available spending and target over time.
|
|
2321
2321
|
|
|
@@ -2326,20 +2326,20 @@ class Plan(object):
|
|
|
2326
2326
|
"""
|
|
2327
2327
|
value = self._checkValue(value)
|
|
2328
2328
|
|
|
2329
|
-
title = self._name +
|
|
2330
|
-
if tag !=
|
|
2331
|
-
title +=
|
|
2329
|
+
title = self._name + "\nNet Available Spending"
|
|
2330
|
+
if tag != "":
|
|
2331
|
+
title += " - " + tag
|
|
2332
2332
|
|
|
2333
|
-
style = {
|
|
2334
|
-
if value ==
|
|
2335
|
-
series = {
|
|
2336
|
-
yformat =
|
|
2333
|
+
style = {"net": "-", "target": ":"}
|
|
2334
|
+
if value == "nominal":
|
|
2335
|
+
series = {"net": self.g_n, "target": (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
|
|
2336
|
+
yformat = "\\$k (nominal)"
|
|
2337
2337
|
else:
|
|
2338
2338
|
series = {
|
|
2339
|
-
|
|
2340
|
-
|
|
2339
|
+
"net": self.g_n / self.gamma_n[:-1],
|
|
2340
|
+
"target": (self.g_n[0] / self.xi_n[0]) * self.xi_n,
|
|
2341
2341
|
}
|
|
2342
|
-
yformat =
|
|
2342
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2343
2343
|
|
|
2344
2344
|
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2345
2345
|
|
|
@@ -2350,7 +2350,7 @@ class Plan(object):
|
|
|
2350
2350
|
return None
|
|
2351
2351
|
|
|
2352
2352
|
@_checkCaseStatus
|
|
2353
|
-
def showAssetDistribution(self, tag=
|
|
2353
|
+
def showAssetDistribution(self, tag="", value=None, figure=False):
|
|
2354
2354
|
"""
|
|
2355
2355
|
Plot the distribution of each savings account in thousands of dollars
|
|
2356
2356
|
during the simulation time. This function will generate three
|
|
@@ -2364,34 +2364,35 @@ class Plan(object):
|
|
|
2364
2364
|
"""
|
|
2365
2365
|
value = self._checkValue(value)
|
|
2366
2366
|
|
|
2367
|
-
if value ==
|
|
2368
|
-
yformat =
|
|
2367
|
+
if value == "nominal":
|
|
2368
|
+
yformat = "\\$k (nominal)"
|
|
2369
2369
|
infladjust = 1
|
|
2370
2370
|
else:
|
|
2371
|
-
yformat =
|
|
2371
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2372
2372
|
infladjust = self.gamma_n
|
|
2373
2373
|
|
|
2374
2374
|
years_n = np.array(self.year_n)
|
|
2375
2375
|
years_n = np.append(years_n, [years_n[-1] + 1])
|
|
2376
2376
|
y2stack = {}
|
|
2377
|
-
jDic = {
|
|
2378
|
-
kDic = {
|
|
2377
|
+
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
2378
|
+
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2379
2379
|
figures = []
|
|
2380
2380
|
for jkey in jDic:
|
|
2381
2381
|
stackNames = []
|
|
2382
2382
|
for kkey in kDic:
|
|
2383
|
-
name = kkey +
|
|
2383
|
+
name = kkey + " / " + jkey
|
|
2384
2384
|
stackNames.append(name)
|
|
2385
2385
|
y2stack[name] = np.zeros((self.N_i, self.N_n + 1))
|
|
2386
2386
|
for i in range(self.N_i):
|
|
2387
2387
|
y2stack[name][i][:] = self.b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
|
|
2388
2388
|
|
|
2389
|
-
title = self._name +
|
|
2390
|
-
if tag !=
|
|
2391
|
-
title +=
|
|
2389
|
+
title = self._name + "\nAssets Distribution - " + jkey
|
|
2390
|
+
if tag != "":
|
|
2391
|
+
title += " - " + tag
|
|
2392
2392
|
|
|
2393
|
-
fig, ax = _stackPlot(
|
|
2394
|
-
|
|
2393
|
+
fig, ax = _stackPlot(
|
|
2394
|
+
years_n, self.inames, title, range(self.N_i), y2stack, stackNames, "upper left", yformat
|
|
2395
|
+
)
|
|
2395
2396
|
figures.append(fig)
|
|
2396
2397
|
|
|
2397
2398
|
if figure:
|
|
@@ -2400,7 +2401,7 @@ class Plan(object):
|
|
|
2400
2401
|
plt.show()
|
|
2401
2402
|
return None
|
|
2402
2403
|
|
|
2403
|
-
def showAllocations(self, tag=
|
|
2404
|
+
def showAllocations(self, tag="", figure=False):
|
|
2404
2405
|
"""
|
|
2405
2406
|
Plot desired allocation of savings accounts in percentage
|
|
2406
2407
|
over simulation time and interpolated by the selected method
|
|
@@ -2409,39 +2410,38 @@ class Plan(object):
|
|
|
2409
2410
|
A tag string can be set to add information to the title of the plot.
|
|
2410
2411
|
"""
|
|
2411
2412
|
count = self.N_i
|
|
2412
|
-
if self.ARCoord ==
|
|
2413
|
+
if self.ARCoord == "spouses":
|
|
2413
2414
|
acList = [self.ARCoord]
|
|
2414
2415
|
count = 1
|
|
2415
|
-
elif self.ARCoord ==
|
|
2416
|
+
elif self.ARCoord == "individual":
|
|
2416
2417
|
acList = [self.ARCoord]
|
|
2417
|
-
elif self.ARCoord ==
|
|
2418
|
-
acList = [
|
|
2418
|
+
elif self.ARCoord == "account":
|
|
2419
|
+
acList = ["taxable", "tax-deferred", "tax-free"]
|
|
2419
2420
|
else:
|
|
2420
|
-
raise ValueError(
|
|
2421
|
+
raise ValueError("Unknown coordination %s" % self.ARCoord)
|
|
2421
2422
|
|
|
2422
2423
|
figures = []
|
|
2423
|
-
assetDic = {
|
|
2424
|
+
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2424
2425
|
for i in range(count):
|
|
2425
2426
|
y2stack = {}
|
|
2426
2427
|
for acType in acList:
|
|
2427
2428
|
stackNames = []
|
|
2428
2429
|
for key in assetDic:
|
|
2429
|
-
aname = key +
|
|
2430
|
+
aname = key + " / " + acType
|
|
2430
2431
|
stackNames.append(aname)
|
|
2431
2432
|
y2stack[aname] = np.zeros((count, self.N_n))
|
|
2432
2433
|
y2stack[aname][i][:] = self.alpha_ijkn[i, acList.index(acType), assetDic[key], : self.N_n]
|
|
2433
2434
|
|
|
2434
|
-
title = self._name +
|
|
2435
|
-
if self.ARCoord ==
|
|
2436
|
-
title +=
|
|
2435
|
+
title = self._name + "\nAsset Allocation (%) - " + acType
|
|
2436
|
+
if self.ARCoord == "spouses":
|
|
2437
|
+
title += " spouses"
|
|
2437
2438
|
else:
|
|
2438
|
-
title +=
|
|
2439
|
+
title += " " + self.inames[i]
|
|
2439
2440
|
|
|
2440
|
-
if tag !=
|
|
2441
|
-
title +=
|
|
2441
|
+
if tag != "":
|
|
2442
|
+
title += " - " + tag
|
|
2442
2443
|
|
|
2443
|
-
fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack,
|
|
2444
|
-
stackNames, 'upper left', 'percent')
|
|
2444
|
+
fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack, stackNames, "upper left", "percent")
|
|
2445
2445
|
figures.append(fig)
|
|
2446
2446
|
|
|
2447
2447
|
if figure:
|
|
@@ -2451,7 +2451,7 @@ class Plan(object):
|
|
|
2451
2451
|
return None
|
|
2452
2452
|
|
|
2453
2453
|
@_checkCaseStatus
|
|
2454
|
-
def showAccounts(self, tag=
|
|
2454
|
+
def showAccounts(self, tag="", value=None, figure=False):
|
|
2455
2455
|
"""
|
|
2456
2456
|
Plot values of savings accounts over time.
|
|
2457
2457
|
|
|
@@ -2462,25 +2462,24 @@ class Plan(object):
|
|
|
2462
2462
|
"""
|
|
2463
2463
|
value = self._checkValue(value)
|
|
2464
2464
|
|
|
2465
|
-
title = self._name +
|
|
2466
|
-
if tag !=
|
|
2467
|
-
title +=
|
|
2465
|
+
title = self._name + "\nSavings Balance"
|
|
2466
|
+
if tag != "":
|
|
2467
|
+
title += " - " + tag
|
|
2468
2468
|
|
|
2469
2469
|
stypes = self.savings_in.keys()
|
|
2470
2470
|
# Add one year for estate.
|
|
2471
2471
|
year_n = np.append(self.year_n, [self.year_n[-1] + 1])
|
|
2472
2472
|
|
|
2473
|
-
if value ==
|
|
2474
|
-
yformat =
|
|
2473
|
+
if value == "nominal":
|
|
2474
|
+
yformat = "\\$k (nominal)"
|
|
2475
2475
|
savings_in = self.savings_in
|
|
2476
2476
|
else:
|
|
2477
|
-
yformat =
|
|
2477
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2478
2478
|
savings_in = {}
|
|
2479
2479
|
for key in self.savings_in:
|
|
2480
2480
|
savings_in[key] = self.savings_in[key] / self.gamma_n
|
|
2481
2481
|
|
|
2482
|
-
fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i),
|
|
2483
|
-
savings_in, stypes, 'upper left', yformat)
|
|
2482
|
+
fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i), savings_in, stypes, "upper left", yformat)
|
|
2484
2483
|
|
|
2485
2484
|
if figure:
|
|
2486
2485
|
return fig
|
|
@@ -2489,7 +2488,7 @@ class Plan(object):
|
|
|
2489
2488
|
return None
|
|
2490
2489
|
|
|
2491
2490
|
@_checkCaseStatus
|
|
2492
|
-
def showSources(self, tag=
|
|
2491
|
+
def showSources(self, tag="", value=None, figure=False):
|
|
2493
2492
|
"""
|
|
2494
2493
|
Plot income over time.
|
|
2495
2494
|
|
|
@@ -2500,23 +2499,24 @@ class Plan(object):
|
|
|
2500
2499
|
"""
|
|
2501
2500
|
value = self._checkValue(value)
|
|
2502
2501
|
|
|
2503
|
-
title = self._name +
|
|
2502
|
+
title = self._name + "\nRaw Income Sources"
|
|
2504
2503
|
stypes = self.sources_in.keys()
|
|
2505
2504
|
|
|
2506
|
-
if tag !=
|
|
2507
|
-
title +=
|
|
2505
|
+
if tag != "":
|
|
2506
|
+
title += " - " + tag
|
|
2508
2507
|
|
|
2509
|
-
if value ==
|
|
2510
|
-
yformat =
|
|
2508
|
+
if value == "nominal":
|
|
2509
|
+
yformat = "\\$k (nominal)"
|
|
2511
2510
|
sources_in = self.sources_in
|
|
2512
2511
|
else:
|
|
2513
|
-
yformat =
|
|
2512
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2514
2513
|
sources_in = {}
|
|
2515
2514
|
for key in self.sources_in:
|
|
2516
2515
|
sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
|
|
2517
2516
|
|
|
2518
|
-
fig, ax = _stackPlot(
|
|
2519
|
-
|
|
2517
|
+
fig, ax = _stackPlot(
|
|
2518
|
+
self.year_n, self.inames, title, range(self.N_i), sources_in, stypes, "upper left", yformat
|
|
2519
|
+
)
|
|
2520
2520
|
|
|
2521
2521
|
if figure:
|
|
2522
2522
|
return fig
|
|
@@ -2525,34 +2525,34 @@ class Plan(object):
|
|
|
2525
2525
|
return None
|
|
2526
2526
|
|
|
2527
2527
|
@_checkCaseStatus
|
|
2528
|
-
def _showFeff(self, tag=
|
|
2528
|
+
def _showFeff(self, tag=""):
|
|
2529
2529
|
"""
|
|
2530
2530
|
Plot income tax paid over time.
|
|
2531
2531
|
|
|
2532
2532
|
A tag string can be set to add information to the title of the plot.
|
|
2533
2533
|
"""
|
|
2534
|
-
title = self._name +
|
|
2535
|
-
if tag !=
|
|
2536
|
-
title +=
|
|
2534
|
+
title = self._name + "\nEff f "
|
|
2535
|
+
if tag != "":
|
|
2536
|
+
title += " - " + tag
|
|
2537
2537
|
|
|
2538
|
-
various = [
|
|
2538
|
+
various = ["-", "--", "-.", ":"]
|
|
2539
2539
|
style = {}
|
|
2540
2540
|
series = {}
|
|
2541
2541
|
q = 0
|
|
2542
2542
|
for t in range(self.N_t):
|
|
2543
|
-
key =
|
|
2543
|
+
key = "f " + str(t)
|
|
2544
2544
|
series[key] = self.F_tn[t] / self.DeltaBar_tn[t]
|
|
2545
2545
|
# print(key, series[key])
|
|
2546
2546
|
style[key] = various[q % len(various)]
|
|
2547
2547
|
q += 1
|
|
2548
2548
|
|
|
2549
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat=
|
|
2549
|
+
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="")
|
|
2550
2550
|
|
|
2551
2551
|
plt.show()
|
|
2552
2552
|
return None
|
|
2553
2553
|
|
|
2554
2554
|
@_checkCaseStatus
|
|
2555
|
-
def showTaxes(self, tag=
|
|
2555
|
+
def showTaxes(self, tag="", value=None, figure=False):
|
|
2556
2556
|
"""
|
|
2557
2557
|
Plot income tax paid over time.
|
|
2558
2558
|
|
|
@@ -2563,21 +2563,21 @@ class Plan(object):
|
|
|
2563
2563
|
"""
|
|
2564
2564
|
value = self._checkValue(value)
|
|
2565
2565
|
|
|
2566
|
-
style = {
|
|
2566
|
+
style = {"income taxes": "-", "Medicare": "-."}
|
|
2567
2567
|
|
|
2568
|
-
if value ==
|
|
2569
|
-
series = {
|
|
2570
|
-
yformat =
|
|
2568
|
+
if value == "nominal":
|
|
2569
|
+
series = {"income taxes": self.T_n, "Medicare": self.M_n}
|
|
2570
|
+
yformat = "\\$k (nominal)"
|
|
2571
2571
|
else:
|
|
2572
2572
|
series = {
|
|
2573
|
-
|
|
2574
|
-
|
|
2573
|
+
"income taxes": self.T_n / self.gamma_n[:-1],
|
|
2574
|
+
"Medicare": self.M_n / self.gamma_n[:-1],
|
|
2575
2575
|
}
|
|
2576
|
-
yformat =
|
|
2576
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2577
2577
|
|
|
2578
|
-
title = self._name +
|
|
2579
|
-
if tag !=
|
|
2580
|
-
title +=
|
|
2578
|
+
title = self._name + "\nIncome Tax"
|
|
2579
|
+
if tag != "":
|
|
2580
|
+
title += " - " + tag
|
|
2581
2581
|
|
|
2582
2582
|
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2583
2583
|
|
|
@@ -2588,7 +2588,7 @@ class Plan(object):
|
|
|
2588
2588
|
return None
|
|
2589
2589
|
|
|
2590
2590
|
@_checkCaseStatus
|
|
2591
|
-
def showGrossIncome(self, tag=
|
|
2591
|
+
def showGrossIncome(self, tag="", value=None, figure=False):
|
|
2592
2592
|
"""
|
|
2593
2593
|
Plot income tax and taxable income over time horizon.
|
|
2594
2594
|
|
|
@@ -2599,30 +2599,30 @@ class Plan(object):
|
|
|
2599
2599
|
"""
|
|
2600
2600
|
value = self._checkValue(value)
|
|
2601
2601
|
|
|
2602
|
-
style = {
|
|
2602
|
+
style = {"taxable income": "-"}
|
|
2603
2603
|
|
|
2604
|
-
if value ==
|
|
2605
|
-
series = {
|
|
2606
|
-
yformat =
|
|
2604
|
+
if value == "nominal":
|
|
2605
|
+
series = {"taxable income": self.G_n}
|
|
2606
|
+
yformat = "\\$k (nominal)"
|
|
2607
2607
|
infladjust = self.gamma_n[:-1]
|
|
2608
2608
|
else:
|
|
2609
|
-
series = {
|
|
2610
|
-
yformat =
|
|
2609
|
+
series = {"taxable income": self.G_n / self.gamma_n[:-1]}
|
|
2610
|
+
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2611
2611
|
infladjust = 1
|
|
2612
2612
|
|
|
2613
|
-
title = self._name +
|
|
2614
|
-
if tag !=
|
|
2615
|
-
title +=
|
|
2613
|
+
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2614
|
+
if tag != "":
|
|
2615
|
+
title += " - " + tag
|
|
2616
2616
|
|
|
2617
2617
|
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2618
2618
|
|
|
2619
2619
|
data = tx.taxBrackets(self.N_i, self.n_d, self.N_n)
|
|
2620
2620
|
for key in data:
|
|
2621
2621
|
data_adj = data[key] * infladjust
|
|
2622
|
-
ax.plot(self.year_n, data_adj, label=key, ls=
|
|
2622
|
+
ax.plot(self.year_n, data_adj, label=key, ls=":")
|
|
2623
2623
|
|
|
2624
|
-
plt.grid(visible=
|
|
2625
|
-
ax.legend(loc=
|
|
2624
|
+
plt.grid(visible="both")
|
|
2625
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2626
2626
|
|
|
2627
2627
|
if figure:
|
|
2628
2628
|
return fig
|
|
@@ -2636,7 +2636,7 @@ class Plan(object):
|
|
|
2636
2636
|
Save parameters in a configuration file.
|
|
2637
2637
|
"""
|
|
2638
2638
|
if basename is None:
|
|
2639
|
-
basename =
|
|
2639
|
+
basename = "case_" + self._name
|
|
2640
2640
|
|
|
2641
2641
|
config.saveConfig(self, basename, self.mylog)
|
|
2642
2642
|
|
|
@@ -2678,10 +2678,11 @@ class Plan(object):
|
|
|
2678
2678
|
|
|
2679
2679
|
Last worksheet contains summary.
|
|
2680
2680
|
"""
|
|
2681
|
+
|
|
2681
2682
|
def fillsheet(sheet, dic, datatype, op=lambda x: x):
|
|
2682
2683
|
rawData = {}
|
|
2683
|
-
rawData[
|
|
2684
|
-
if datatype ==
|
|
2684
|
+
rawData["year"] = self.year_n
|
|
2685
|
+
if datatype == "currency":
|
|
2685
2686
|
for key in dic:
|
|
2686
2687
|
rawData[key] = u.roundCents(op(dic[key]))
|
|
2687
2688
|
else:
|
|
@@ -2699,71 +2700,71 @@ class Plan(object):
|
|
|
2699
2700
|
|
|
2700
2701
|
# Income.
|
|
2701
2702
|
ws = wb.active
|
|
2702
|
-
ws.title =
|
|
2703
|
+
ws.title = "Income"
|
|
2703
2704
|
|
|
2704
2705
|
incomeDic = {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2706
|
+
"net spending": self.g_n,
|
|
2707
|
+
"taxable ord. income": self.G_n,
|
|
2708
|
+
"taxable gains/divs": self.Q_n,
|
|
2709
|
+
"Tax bills + Med.": self.T_n + self.U_n + self.M_n,
|
|
2709
2710
|
}
|
|
2710
2711
|
|
|
2711
|
-
fillsheet(ws, incomeDic,
|
|
2712
|
+
fillsheet(ws, incomeDic, "currency")
|
|
2712
2713
|
|
|
2713
2714
|
# Cash flow - sum over both individuals for some.
|
|
2714
2715
|
cashFlowDic = {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2716
|
+
"net spending": self.g_n,
|
|
2717
|
+
"all wages": np.sum(self.omega_in, axis=0),
|
|
2718
|
+
"all pensions": np.sum(self.piBar_in, axis=0),
|
|
2719
|
+
"all soc sec": np.sum(self.zetaBar_in, axis=0),
|
|
2719
2720
|
"all BTI's": np.sum(self.Lambda_in, axis=0),
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2721
|
+
"all wdrwls": np.sum(self.w_ijn, axis=(0, 1)),
|
|
2722
|
+
"all deposits": -np.sum(self.d_in, axis=0),
|
|
2723
|
+
"ord taxes": -self.T_n,
|
|
2724
|
+
"div taxes": -self.U_n,
|
|
2725
|
+
"Medicare": -self.M_n,
|
|
2725
2726
|
}
|
|
2726
|
-
sname =
|
|
2727
|
+
sname = "Cash Flow"
|
|
2727
2728
|
ws = wb.create_sheet(sname)
|
|
2728
|
-
fillsheet(ws, cashFlowDic,
|
|
2729
|
+
fillsheet(ws, cashFlowDic, "currency")
|
|
2729
2730
|
|
|
2730
2731
|
# Sources are handled separately.
|
|
2731
2732
|
srcDic = {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2733
|
+
"wages": self.sources_in["wages"],
|
|
2734
|
+
"social sec": self.sources_in["ssec"],
|
|
2735
|
+
"pension": self.sources_in["pension"],
|
|
2736
|
+
"txbl acc wdrwl": self.sources_in["txbl acc wdrwl"],
|
|
2737
|
+
"RMDs": self.sources_in["RMD"],
|
|
2738
|
+
"+distributions": self.sources_in["+dist"],
|
|
2739
|
+
"Roth conv": self.sources_in["RothX"],
|
|
2740
|
+
"tax-free wdrwl": self.sources_in["tax-free wdrwl"],
|
|
2741
|
+
"big-ticket items": self.sources_in["BTI"],
|
|
2741
2742
|
}
|
|
2742
2743
|
|
|
2743
2744
|
for i in range(self.N_i):
|
|
2744
2745
|
sname = self.inames[i] + "'s Sources"
|
|
2745
2746
|
ws = wb.create_sheet(sname)
|
|
2746
|
-
fillsheet(ws, srcDic,
|
|
2747
|
+
fillsheet(ws, srcDic, "currency", op=lambda x: x[i])
|
|
2747
2748
|
|
|
2748
2749
|
# Account balances except final year.
|
|
2749
2750
|
accDic = {
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2751
|
+
"taxable bal": self.b_ijn[:, 0, :-1],
|
|
2752
|
+
"taxable ctrb": self.kappa_ijn[:, 0, :],
|
|
2753
|
+
"taxable dep": self.d_in,
|
|
2754
|
+
"taxable wdrwl": self.w_ijn[:, 0, :],
|
|
2755
|
+
"tax-deferred bal": self.b_ijn[:, 1, :-1],
|
|
2756
|
+
"tax-deferred ctrb": self.kappa_ijn[:, 1, :],
|
|
2757
|
+
"tax-deferred wdrwl": self.w_ijn[:, 1, :],
|
|
2758
|
+
"(included RMDs)": self.rmd_in[:, :],
|
|
2759
|
+
"Roth conv": self.x_in,
|
|
2760
|
+
"tax-free bal": self.b_ijn[:, 2, :-1],
|
|
2761
|
+
"tax-free ctrb": self.kappa_ijn[:, 2, :],
|
|
2762
|
+
"tax-free wdrwl": self.w_ijn[:, 2, :],
|
|
2762
2763
|
}
|
|
2763
2764
|
for i in range(self.N_i):
|
|
2764
2765
|
sname = self.inames[i] + "'s Accounts"
|
|
2765
2766
|
ws = wb.create_sheet(sname)
|
|
2766
|
-
fillsheet(ws, accDic,
|
|
2767
|
+
fillsheet(ws, accDic, "currency", op=lambda x: x[i])
|
|
2767
2768
|
# Add final balances.
|
|
2768
2769
|
lastRow = [
|
|
2769
2770
|
self.year_n[-1] + 1,
|
|
@@ -2781,11 +2782,11 @@ class Plan(object):
|
|
|
2781
2782
|
0,
|
|
2782
2783
|
]
|
|
2783
2784
|
ws.append(lastRow)
|
|
2784
|
-
_formatSpreadsheet(ws,
|
|
2785
|
+
_formatSpreadsheet(ws, "currency")
|
|
2785
2786
|
|
|
2786
2787
|
# Allocations.
|
|
2787
|
-
jDic = {
|
|
2788
|
-
kDic = {
|
|
2788
|
+
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
2789
|
+
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2789
2790
|
|
|
2790
2791
|
# Add one year for estate.
|
|
2791
2792
|
year_n = np.append(self.year_n, [self.year_n[-1] + 1])
|
|
@@ -2793,28 +2794,30 @@ class Plan(object):
|
|
|
2793
2794
|
sname = self.inames[i] + "'s Allocations"
|
|
2794
2795
|
ws = wb.create_sheet(sname)
|
|
2795
2796
|
rawData = {}
|
|
2796
|
-
rawData[
|
|
2797
|
+
rawData["year"] = year_n
|
|
2797
2798
|
for jkey in jDic:
|
|
2798
2799
|
for kkey in kDic:
|
|
2799
|
-
rawData[jkey +
|
|
2800
|
+
rawData[jkey + "/" + kkey] = self.alpha_ijkn[i, jDic[jkey], kDic[kkey], :]
|
|
2800
2801
|
df = pd.DataFrame(rawData)
|
|
2801
2802
|
for row in dataframe_to_rows(df, index=False, header=True):
|
|
2802
2803
|
ws.append(row)
|
|
2803
2804
|
|
|
2804
|
-
_formatSpreadsheet(ws,
|
|
2805
|
+
_formatSpreadsheet(ws, "percent1")
|
|
2805
2806
|
|
|
2806
2807
|
# Rates on penultimate sheet.
|
|
2807
|
-
ratesDic = {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2808
|
+
ratesDic = {
|
|
2809
|
+
"S&P 500": self.tau_kn[0],
|
|
2810
|
+
"Corporate Baa": self.tau_kn[1],
|
|
2811
|
+
"T Bonds": self.tau_kn[2],
|
|
2812
|
+
"inflation": self.tau_kn[3],
|
|
2813
|
+
}
|
|
2814
|
+
ws = wb.create_sheet("Rates")
|
|
2815
|
+
fillsheet(ws, ratesDic, "percent2")
|
|
2813
2816
|
|
|
2814
2817
|
# Summary on last sheet.
|
|
2815
|
-
ws = wb.create_sheet(
|
|
2818
|
+
ws = wb.create_sheet("Summary")
|
|
2816
2819
|
rawData = {}
|
|
2817
|
-
rawData[
|
|
2820
|
+
rawData["SUMMARY ==========================================================================="] = (
|
|
2818
2821
|
self.summaryList()
|
|
2819
2822
|
)
|
|
2820
2823
|
|
|
@@ -2822,7 +2825,7 @@ class Plan(object):
|
|
|
2822
2825
|
for row in dataframe_to_rows(df, index=False, header=True):
|
|
2823
2826
|
ws.append(row)
|
|
2824
2827
|
|
|
2825
|
-
_formatSpreadsheet(ws,
|
|
2828
|
+
_formatSpreadsheet(ws, "summary")
|
|
2826
2829
|
|
|
2827
2830
|
if saveToFile:
|
|
2828
2831
|
if basename is None:
|
|
@@ -2841,27 +2844,27 @@ class Plan(object):
|
|
|
2841
2844
|
"""
|
|
2842
2845
|
|
|
2843
2846
|
planData = {}
|
|
2844
|
-
planData[
|
|
2845
|
-
planData[
|
|
2846
|
-
planData[
|
|
2847
|
-
planData[
|
|
2848
|
-
planData[
|
|
2847
|
+
planData["year"] = self.year_n
|
|
2848
|
+
planData["net spending"] = self.g_n
|
|
2849
|
+
planData["taxable ord. income"] = self.G_n
|
|
2850
|
+
planData["taxable gains/divs"] = self.Q_n
|
|
2851
|
+
planData["tax bill"] = self.T_n
|
|
2849
2852
|
|
|
2850
2853
|
for i in range(self.N_i):
|
|
2851
|
-
planData[self.inames[i] +
|
|
2852
|
-
planData[self.inames[i] +
|
|
2853
|
-
planData[self.inames[i] +
|
|
2854
|
-
planData[self.inames[i] +
|
|
2855
|
-
planData[self.inames[i] +
|
|
2856
|
-
planData[self.inames[i] +
|
|
2857
|
-
planData[self.inames[i] +
|
|
2858
|
-
planData[self.inames[i] +
|
|
2859
|
-
planData[self.inames[i] +
|
|
2860
|
-
planData[self.inames[i] +
|
|
2861
|
-
planData[self.inames[i] +
|
|
2862
|
-
planData[self.inames[i] +
|
|
2863
|
-
|
|
2864
|
-
ratesDic = {
|
|
2854
|
+
planData[self.inames[i] + " txbl bal"] = self.b_ijn[i, 0, :-1]
|
|
2855
|
+
planData[self.inames[i] + " txbl dep"] = self.d_in[i, :]
|
|
2856
|
+
planData[self.inames[i] + " txbl wrdwl"] = self.w_ijn[i, 0, :]
|
|
2857
|
+
planData[self.inames[i] + " tx-def bal"] = self.b_ijn[i, 1, :-1]
|
|
2858
|
+
planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :]
|
|
2859
|
+
planData[self.inames[i] + " tx-def wdrl"] = self.w_ijn[i, 1, :]
|
|
2860
|
+
planData[self.inames[i] + " (RMD)"] = self.rmd_in[i, :]
|
|
2861
|
+
planData[self.inames[i] + " Roth conv"] = self.x_in[i, :]
|
|
2862
|
+
planData[self.inames[i] + " tx-free bal"] = self.b_ijn[i, 2, :-1]
|
|
2863
|
+
planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :]
|
|
2864
|
+
planData[self.inames[i] + " tax-free wdrwl"] = self.w_ijn[i, 2, :]
|
|
2865
|
+
planData[self.inames[i] + " big-ticket items"] = self.Lambda_in[i, :]
|
|
2866
|
+
|
|
2867
|
+
ratesDic = {"S&P 500": 0, "Corporate Baa": 1, "T Bonds": 2, "inflation": 3}
|
|
2865
2868
|
for key in ratesDic:
|
|
2866
2869
|
planData[key] = self.tau_kn[ratesDic[key]]
|
|
2867
2870
|
|
|
@@ -2869,39 +2872,39 @@ class Plan(object):
|
|
|
2869
2872
|
|
|
2870
2873
|
while True:
|
|
2871
2874
|
try:
|
|
2872
|
-
fname =
|
|
2875
|
+
fname = "worksheet" + "_" + basename + ".csv"
|
|
2873
2876
|
df.to_csv(fname)
|
|
2874
2877
|
break
|
|
2875
2878
|
except PermissionError:
|
|
2876
|
-
self.mylog.print('Failed to save "%s": %s.' % (fname,
|
|
2877
|
-
key = input(
|
|
2878
|
-
if key ==
|
|
2879
|
+
self.mylog.print('Failed to save "%s": %s.' % (fname, "Permission denied"))
|
|
2880
|
+
key = input("Close file and try again? [Yn] ")
|
|
2881
|
+
if key == "n":
|
|
2879
2882
|
break
|
|
2880
2883
|
except Exception as e:
|
|
2881
|
-
raise Exception(
|
|
2884
|
+
raise Exception("Unanticipated exception %r." % e)
|
|
2882
2885
|
|
|
2883
2886
|
return None
|
|
2884
2887
|
|
|
2885
2888
|
|
|
2886
|
-
def _lineIncomePlot(x, series, style, title, yformat=
|
|
2889
|
+
def _lineIncomePlot(x, series, style, title, yformat="\\$k"):
|
|
2887
2890
|
"""
|
|
2888
2891
|
Core line plotter function.
|
|
2889
2892
|
"""
|
|
2890
2893
|
import matplotlib.ticker as tk
|
|
2891
2894
|
|
|
2892
2895
|
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2893
|
-
plt.grid(visible=
|
|
2896
|
+
plt.grid(visible="both")
|
|
2894
2897
|
|
|
2895
2898
|
for sname in series:
|
|
2896
2899
|
ax.plot(x, series[sname], label=sname, ls=style[sname])
|
|
2897
2900
|
|
|
2898
|
-
ax.legend(loc=
|
|
2901
|
+
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2899
2902
|
ax.set_title(title)
|
|
2900
|
-
ax.set_xlabel(
|
|
2903
|
+
ax.set_xlabel("year")
|
|
2901
2904
|
ax.set_ylabel(yformat)
|
|
2902
2905
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2903
|
-
if
|
|
2904
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000),
|
|
2906
|
+
if "k" in yformat:
|
|
2907
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
2905
2908
|
# Give range to y values in unindexed flat profiles.
|
|
2906
2909
|
ymin, ymax = ax.get_ylim()
|
|
2907
2910
|
if ymax - ymin < 5000:
|
|
@@ -2910,7 +2913,7 @@ def _lineIncomePlot(x, series, style, title, yformat='\\$k'):
|
|
|
2910
2913
|
return fig, ax
|
|
2911
2914
|
|
|
2912
2915
|
|
|
2913
|
-
def _stackPlot(x, inames, title, irange, series, snames, location, yformat=
|
|
2916
|
+
def _stackPlot(x, inames, title, irange, series, snames, location, yformat="\\$k"):
|
|
2914
2917
|
"""
|
|
2915
2918
|
Core function for stacked plots.
|
|
2916
2919
|
"""
|
|
@@ -2921,28 +2924,28 @@ def _stackPlot(x, inames, title, irange, series, snames, location, yformat='\\$k
|
|
|
2921
2924
|
for i in irange:
|
|
2922
2925
|
tmp = series[sname][i]
|
|
2923
2926
|
if sum(tmp) > 1.0:
|
|
2924
|
-
nonzeroSeries[sname +
|
|
2927
|
+
nonzeroSeries[sname + " " + inames[i]] = tmp
|
|
2925
2928
|
|
|
2926
2929
|
if len(nonzeroSeries) == 0:
|
|
2927
2930
|
# print('Nothing to plot for', title)
|
|
2928
2931
|
return None
|
|
2929
2932
|
|
|
2930
2933
|
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2931
|
-
plt.grid(visible=
|
|
2934
|
+
plt.grid(visible="both")
|
|
2932
2935
|
|
|
2933
2936
|
ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
|
|
2934
2937
|
ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
|
|
2935
2938
|
ax.set_title(title)
|
|
2936
|
-
ax.set_xlabel(
|
|
2939
|
+
ax.set_xlabel("year")
|
|
2937
2940
|
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2938
|
-
if
|
|
2941
|
+
if "k" in yformat:
|
|
2939
2942
|
ax.set_ylabel(yformat)
|
|
2940
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000),
|
|
2941
|
-
elif yformat ==
|
|
2942
|
-
ax.set_ylabel(
|
|
2943
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x),
|
|
2943
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
2944
|
+
elif yformat == "percent":
|
|
2945
|
+
ax.set_ylabel("%")
|
|
2946
|
+
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
|
|
2944
2947
|
else:
|
|
2945
|
-
raise RuntimeError(
|
|
2948
|
+
raise RuntimeError("Unknown yformat: %s." % yformat)
|
|
2946
2949
|
|
|
2947
2950
|
return fig, ax
|
|
2948
2951
|
|
|
@@ -2955,15 +2958,15 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2955
2958
|
from pathlib import Path
|
|
2956
2959
|
|
|
2957
2960
|
if Path(basename).suffixes == []:
|
|
2958
|
-
fname =
|
|
2961
|
+
fname = "workbook" + "_" + basename + ".xlsx"
|
|
2959
2962
|
else:
|
|
2960
2963
|
fname = basename
|
|
2961
2964
|
|
|
2962
2965
|
if overwrite is False and isfile(fname):
|
|
2963
2966
|
mylog.print('File "%s" already exists.' % fname)
|
|
2964
|
-
key = input(
|
|
2965
|
-
if key !=
|
|
2966
|
-
mylog.vprint(
|
|
2967
|
+
key = input("Overwrite? [Ny] ")
|
|
2968
|
+
if key != "y":
|
|
2969
|
+
mylog.vprint("Skipping save and returning.")
|
|
2967
2970
|
return None
|
|
2968
2971
|
|
|
2969
2972
|
while True:
|
|
@@ -2972,12 +2975,12 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
2972
2975
|
wb.save(fname)
|
|
2973
2976
|
break
|
|
2974
2977
|
except PermissionError:
|
|
2975
|
-
mylog.print('Failed to save "%s": %s.' % (fname,
|
|
2976
|
-
key = input(
|
|
2977
|
-
if key ==
|
|
2978
|
+
mylog.print('Failed to save "%s": %s.' % (fname, "Permission denied"))
|
|
2979
|
+
key = input("Close file and try again? [Yn] ")
|
|
2980
|
+
if key == "n":
|
|
2978
2981
|
break
|
|
2979
2982
|
except Exception as e:
|
|
2980
|
-
raise Exception(
|
|
2983
|
+
raise Exception("Unanticipated exception %r." % e)
|
|
2981
2984
|
|
|
2982
2985
|
return None
|
|
2983
2986
|
|
|
@@ -2986,31 +2989,31 @@ def _formatSpreadsheet(ws, ftype):
|
|
|
2986
2989
|
"""
|
|
2987
2990
|
Utility function to beautify spreadsheet.
|
|
2988
2991
|
"""
|
|
2989
|
-
if ftype ==
|
|
2990
|
-
fstring =
|
|
2991
|
-
elif ftype ==
|
|
2992
|
-
fstring =
|
|
2993
|
-
elif ftype ==
|
|
2994
|
-
fstring =
|
|
2995
|
-
elif ftype ==
|
|
2996
|
-
fstring =
|
|
2997
|
-
elif ftype ==
|
|
2992
|
+
if ftype == "currency":
|
|
2993
|
+
fstring = "$#,##0_);[Red]($#,##0)"
|
|
2994
|
+
elif ftype == "percent2":
|
|
2995
|
+
fstring = "#.00%"
|
|
2996
|
+
elif ftype == "percent1":
|
|
2997
|
+
fstring = "#.0%"
|
|
2998
|
+
elif ftype == "percent0":
|
|
2999
|
+
fstring = "#0%"
|
|
3000
|
+
elif ftype == "summary":
|
|
2998
3001
|
for col in ws.columns:
|
|
2999
3002
|
column = col[0].column_letter
|
|
3000
3003
|
width = max(len(str(col[0].value)) + 20, 40)
|
|
3001
3004
|
ws.column_dimensions[column].width = width
|
|
3002
3005
|
return None
|
|
3003
3006
|
else:
|
|
3004
|
-
raise RuntimeError(
|
|
3007
|
+
raise RuntimeError("Unknown format: %s." % ftype)
|
|
3005
3008
|
|
|
3006
|
-
for cell in ws[1] + ws[
|
|
3007
|
-
cell.style =
|
|
3009
|
+
for cell in ws[1] + ws["A"]:
|
|
3010
|
+
cell.style = "Pandas"
|
|
3008
3011
|
for col in ws.columns:
|
|
3009
3012
|
column = col[0].column_letter
|
|
3010
3013
|
# col[0].style = 'Title'
|
|
3011
3014
|
width = max(len(str(col[0].value)) + 4, 10)
|
|
3012
3015
|
ws.column_dimensions[column].width = width
|
|
3013
|
-
if column !=
|
|
3016
|
+
if column != "A":
|
|
3014
3017
|
for cell in col:
|
|
3015
3018
|
cell.number_format = fstring
|
|
3016
3019
|
|