Skip to content

Commit 1175e5d

Browse files
committed
Refactor doctor_visits weekday.py
1 parent fa7a8ed commit 1175e5d

File tree

2 files changed

+60
-55
lines changed

2 files changed

+60
-55
lines changed

doctor_visits/delphi_doctor_visits/update_sensor.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,13 @@ def update_sensor(
125125
(burn_in_dates >= startdate) & (burn_in_dates <= enddate))[0][:len(sensor_dates)]
126126

127127
# handle if we need to adjust by weekday
128-
params = Weekday.get_params(data, logger) if weekday else None
128+
params = Weekday.get_params(
129+
data,
130+
"Denominator",
131+
Config.CLI_COLS + Config.FLU1_COL,
132+
[1, 1e5, 1e10, 1e15],
133+
logger,
134+
) if weekday else None
129135
if weekday and np.any(np.all(params == 0,axis=1)):
130136
# Weekday correction failed for at least one count type
131137
return None

doctor_visits/delphi_doctor_visits/weekday.py

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,49 @@
33
44
Created: 2020-05-06
55
"""
6-
7-
8-
9-
# third party
106
import cvxpy as cp
11-
from cvxpy.error import SolverError
127
import numpy as np
8+
from cvxpy.error import SolverError
139

14-
# first party
1510
from .config import Config
1611

1712

1813
class Weekday:
1914
"""Class to handle weekday effects."""
2015

2116
@staticmethod
22-
def get_params(data, logger):
17+
def get_params(data, denominator_col, numerator_cols, scales, logger):
18+
r"""Fit weekday correction for each col in numerator_cols.
19+
20+
Return a matrix of parameters: the entire vector of betas, for each time
21+
series column in the data.
22+
"""
23+
denoms = data.groupby(Config.DATE_COL).sum()[denominator_col]
24+
nums = data.groupby(Config.DATE_COL).sum()[numerator_cols]
25+
26+
# Construct design matrix to have weekday indicator columns and then day
27+
# indicators.
28+
X = np.zeros((nums.shape[0], 6 + nums.shape[0]))
29+
not_sunday = np.where(nums.index.dayofweek != 6)[0]
30+
X[not_sunday, np.array(nums.index.dayofweek)[not_sunday]] = 1
31+
X[np.where(nums.index.dayofweek == 6)[0], :6] = -1
32+
X[:, 6:] = np.eye(X.shape[0])
33+
34+
npnums, npdenoms = np.array(nums), np.array(denoms)
35+
params = np.zeros((nums.shape[1], X.shape[1]))
36+
37+
# Loop over the available numerator columns and smooth each separately.
38+
for i in range(nums.shape[1]):
39+
result = _fit(X, scales, npnums[:, i], npdenoms)
40+
if result is None:
41+
logger.error("Unable to calculate weekday correction")
42+
else:
43+
params[i,:] = result
44+
45+
return params
46+
47+
@staticmethod
48+
def _fit(X, scales, npnums, npdenoms):
2349
r"""Correct a signal estimated as numerator/denominator for weekday effects.
2450
2551
The ordinary estimate would be numerator_t/denominator_t for each time point
@@ -53,54 +79,27 @@ def get_params(data, logger):
5379
5480
ll = (numerator * (X*b + log(denominator)) - sum(exp(X*b) + log(denominator)))
5581
/ num_days
56-
57-
Return a matrix of parameters: the entire vector of betas, for each time
58-
series column in the data.
5982
"""
60-
denoms = data.groupby(Config.DATE_COL).sum()["Denominator"]
61-
nums = data.groupby(Config.DATE_COL).sum()[Config.CLI_COLS + Config.FLU1_COL]
62-
63-
# Construct design matrix to have weekday indicator columns and then day
64-
# indicators.
65-
X = np.zeros((nums.shape[0], 6 + nums.shape[0]))
66-
not_sunday = np.where(nums.index.dayofweek != 6)[0]
67-
X[not_sunday, np.array(nums.index.dayofweek)[not_sunday]] = 1
68-
X[np.where(nums.index.dayofweek == 6)[0], :6] = -1
69-
X[:, 6:] = np.eye(X.shape[0])
70-
71-
npnums, npdenoms = np.array(nums), np.array(denoms)
72-
params = np.zeros((nums.shape[1], X.shape[1]))
73-
74-
# Loop over the available numerator columns and smooth each separately.
75-
for i in range(nums.shape[1]):
76-
b = cp.Variable((X.shape[1]))
77-
78-
lmbda = cp.Parameter(nonneg=True)
79-
lmbda.value = 10 # Hard-coded for now, seems robust to changes
80-
81-
ll = (
82-
cp.matmul(npnums[:, i], cp.matmul(X, b) + np.log(npdenoms))
83-
- cp.sum(cp.exp(cp.matmul(X, b) + np.log(npdenoms)))
84-
) / X.shape[0]
85-
penalty = (
86-
lmbda * cp.norm(cp.diff(b[6:], 3), 1) / (X.shape[0] - 2)
87-
) # L-1 Norm of third differences, rewards smoothness
88-
scales = [1, 1e5, 1e10, 1e15]
89-
for scale in scales:
90-
try:
91-
prob = cp.Problem(cp.Minimize((-ll + lmbda * penalty) / scale))
92-
_ = prob.solve()
93-
params[i,:] = b.value
94-
break
95-
except SolverError:
96-
# If the magnitude of the objective function is too large, an error is
97-
# thrown; Rescale the objective function by going through loop
98-
pass
99-
else:
100-
# Leaving params[i,:] = 0 is equivalent to not performing weekday correction
101-
logger.error("Unable to calculate weekday correction")
102-
103-
return params
83+
b = cp.Variable((X.shape[1]))
84+
85+
lmbda = cp.Parameter(nonneg=True)
86+
lmbda.value = 10 # Hard-coded for now, seems robust to changes
87+
88+
ll = (
89+
cp.matmul(npnums, cp.matmul(X, b) + np.log(npdenoms)) -
90+
cp.sum(cp.exp(cp.matmul(X, b) + np.log(npdenoms)))
91+
) / X.shape[0]
92+
# L-1 Norm of third differences, rewards smoothness
93+
penalty = lmbda * cp.norm(cp.diff(b[6:], 3), 1) / (X.shape[0] - 2)
94+
for scale in scales:
95+
try:
96+
prob = cp.Problem(cp.Minimize((-ll + lmbda * penalty) / scale))
97+
_ = prob.solve()
98+
return b.value
99+
except SolverError:
100+
# If the magnitude of the objective function is too large, an error is
101+
# thrown; Rescale the objective function by going through loop
102+
continue
104103

105104
@staticmethod
106105
def calc_adjustment(params, sub_data, cols):

0 commit comments

Comments
 (0)