Skip to content

Commit 1ec6290

Browse files
committed
add canonical poisson model
1 parent 3badcc0 commit 1ec6290

File tree

3 files changed

+115
-40
lines changed

3 files changed

+115
-40
lines changed

src/regmod/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
from .model import Model
88
from .negativebinomial import NegativeBinomialModel
99
from .pogit import PogitModel
10-
from .poisson import PoissonModel
10+
from .poisson import CanonicalPoissonModel, PoissonModel, create_poisson_model
1111
from .tobit import TobitModel
1212
from .weibull import WeibullModel

src/regmod/models/poisson.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import numpy as np
66
from scipy.stats import poisson
77

8-
from regmod._typing import Callable, DataFrame, NDArray
8+
from regmod._typing import Callable, DataFrame, Matrix, NDArray
99
from regmod.data import Data
1010
from regmod.optimizer import msca_optimize
1111

1212
from .model import Model
13-
from .utils import model_post_init
13+
from .utils import get_params, model_post_init
1414

1515

1616
class PoissonModel(Model):
@@ -34,6 +34,13 @@ def attach_df(self, df: DataFrame):
3434
self.linear_gvec,
3535
)
3636

37+
def get_lin_param(self, coefs: NDArray) -> NDArray:
38+
mat = self.mat[0]
39+
lin_param = mat.dot(coefs)
40+
if self.params[0].offset is not None:
41+
lin_param += self.data.get_cols(self.params[0].offset)
42+
return lin_param
43+
3744
def hessian_from_gprior(self):
3845
return self.hmat
3946

@@ -157,3 +164,62 @@ def d2nll(self, params: list[NDArray]) -> list[list[NDArray]]:
157164
def get_ui(self, params: list[NDArray], bounds: tuple[float, float]) -> NDArray:
158165
mean = params[0]
159166
return [poisson.ppf(bounds[0], mu=mean), poisson.ppf(bounds[1], mu=mean)]
167+
168+
169+
class CanonicalPoissonModel(PoissonModel):
170+
def __init__(self, data: Data, **kwargs):
171+
super().__init__(data, **kwargs)
172+
if self.params[0].inv_link.name != "exp":
173+
raise ValueError("Canonical Poisson model requires inverse link to be exp.")
174+
175+
def objective(self, coefs: NDArray) -> float:
176+
weights = self.data.weights * self.data.trim_weights
177+
y = self.get_lin_param(coefs)
178+
z = np.exp(y)
179+
180+
prior_obj = self.objective_from_gprior(coefs)
181+
likli_obj = weights.dot(z - self.data.obs * y)
182+
return prior_obj + likli_obj
183+
184+
def gradient(self, coefs: NDArray) -> NDArray:
185+
mat = self.mat[0]
186+
weights = self.data.weights * self.data.trim_weights
187+
z = np.exp(self.get_lin_param(coefs))
188+
189+
prior_grad = self.gradient_from_gprior(coefs)
190+
likli_grad = mat.T.dot(weights * (z - self.data.obs))
191+
return prior_grad + likli_grad
192+
193+
def hessian(self, coefs: NDArray) -> Matrix:
194+
mat = self.mat[0]
195+
weights = self.data.weights * self.data.trim_weights
196+
z = np.exp(self.get_lin_param(coefs))
197+
likli_hess_scale = weights * z
198+
199+
prior_hess = self.hessian_from_gprior()
200+
likli_hess_right = mat.scale_rows(likli_hess_scale)
201+
likli_hess = mat.T.dot(likli_hess_right)
202+
203+
return prior_hess + likli_hess
204+
205+
def jacobian2(self, coefs: NDArray) -> NDArray:
206+
mat = self.mat[0]
207+
weights = self.data.weights * self.data.trim_weights
208+
z = np.exp(self.get_lin_param(coefs))
209+
likli_jac_scale = weights * (z - self.data.obs)
210+
211+
likli_jac = mat.T.scale_cols(likli_jac_scale)
212+
likli_jac2 = likli_jac.dot(likli_jac.T)
213+
return self.hessian_from_gprior() + likli_jac2
214+
215+
216+
def create_poisson_model(data: Data, **kwargs) -> PoissonModel:
217+
params = get_params(
218+
params=kwargs.get("params"),
219+
param_specs=kwargs.get("param_specs"),
220+
default_param_specs=PoissonModel.default_param_specs,
221+
)
222+
223+
if params[0].inv_link.name == "exp":
224+
return CanonicalPoissonModel(data, params=params)
225+
return PoissonModel(data, params=params)

tests/test_poissonmodel.py

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
"""
22
Test Poisson Model
33
"""
4+
45
import numpy as np
56
import pandas as pd
67
import pytest
7-
88
from regmod.data import Data
99
from regmod.function import fun_dict
10-
from regmod.models import PoissonModel
11-
from regmod.prior import (GaussianPrior, SplineGaussianPrior,
12-
SplineUniformPrior, UniformPrior)
10+
from regmod.models import create_poisson_model
11+
from regmod.prior import (
12+
GaussianPrior,
13+
SplineGaussianPrior,
14+
SplineUniformPrior,
15+
UniformPrior,
16+
)
1317
from regmod.utils import SplineSpecs
1418
from regmod.variable import SplineVariable, Variable
1519

@@ -19,27 +23,27 @@
1923
@pytest.fixture
2024
def data():
2125
num_obs = 5
22-
df = pd.DataFrame({
23-
"obs": np.random.rand(num_obs)*10,
24-
"cov0": np.random.randn(num_obs),
25-
"cov1": np.random.randn(num_obs)
26-
})
27-
return Data(col_obs="obs",
28-
col_covs=["cov0", "cov1"],
29-
df=df)
26+
df = pd.DataFrame(
27+
{
28+
"obs": np.random.rand(num_obs) * 10,
29+
"cov0": np.random.randn(num_obs),
30+
"cov1": np.random.randn(num_obs),
31+
}
32+
)
33+
return Data(col_obs="obs", col_covs=["cov0", "cov1"], df=df)
3034

3135

3236
@pytest.fixture
3337
def wrong_data():
3438
num_obs = 5
35-
df = pd.DataFrame({
36-
"obs": np.random.randn(num_obs),
37-
"cov0": np.random.randn(num_obs),
38-
"cov1": np.random.randn(num_obs)
39-
})
40-
return Data(col_obs="obs",
41-
col_covs=["cov0", "cov1"],
42-
df=df)
39+
df = pd.DataFrame(
40+
{
41+
"obs": np.random.randn(num_obs),
42+
"cov0": np.random.randn(num_obs),
43+
"cov1": np.random.randn(num_obs),
44+
}
45+
)
46+
return Data(col_obs="obs", col_covs=["cov0", "cov1"], df=df)
4347

4448

4549
@pytest.fixture
@@ -54,9 +58,9 @@ def uprior():
5458

5559
@pytest.fixture
5660
def spline_specs():
57-
return SplineSpecs(knots=np.linspace(0.0, 1.0, 5),
58-
degree=3,
59-
knots_type="rel_domain")
61+
return SplineSpecs(
62+
knots=np.linspace(0.0, 1.0, 5), degree=3, knots_type="rel_domain"
63+
)
6064

6165

6266
@pytest.fixture
@@ -71,20 +75,21 @@ def spline_uprior():
7175

7276
@pytest.fixture
7377
def var_cov0(gprior, uprior):
74-
return Variable(name="cov0",
75-
priors=[gprior, uprior])
78+
return Variable(name="cov0", priors=[gprior, uprior])
7679

7780

7881
@pytest.fixture
7982
def var_cov1(spline_gprior, spline_uprior, spline_specs):
80-
return SplineVariable(name="cov1",
81-
spline_specs=spline_specs,
82-
priors=[spline_gprior, spline_uprior])
83+
return SplineVariable(
84+
name="cov1", spline_specs=spline_specs, priors=[spline_gprior, spline_uprior]
85+
)
8386

8487

8588
@pytest.fixture
8689
def model(data, var_cov0, var_cov1):
87-
return PoissonModel(data, param_specs={"lam": {"variables": [var_cov0, var_cov1]}})
90+
return create_poisson_model(
91+
data, param_specs={"lam": {"variables": [var_cov0, var_cov1]}}
92+
)
8893

8994

9095
def test_model_size(model, var_cov0, var_cov1):
@@ -124,7 +129,7 @@ def test_model_gradient(model, inv_link):
124129
tr_grad = np.zeros(model.size)
125130
for i in range(model.size):
126131
coefs_c[i] += 1e-16j
127-
tr_grad[i] = model.objective(coefs_c).imag/1e-16
132+
tr_grad[i] = model.objective(coefs_c).imag / 1e-16
128133
coefs_c[i] -= 1e-16j
129134
assert np.allclose(my_grad, tr_grad)
130135

@@ -139,15 +144,17 @@ def test_model_hessian(model, inv_link):
139144
for i in range(model.size):
140145
for j in range(model.size):
141146
coefs_c[j] += 1e-16j
142-
tr_hess[i][j] = model.gradient(coefs_c).imag[i]/1e-16
147+
tr_hess[i][j] = model.gradient(coefs_c).imag[i] / 1e-16
143148
coefs_c[j] -= 1e-16j
144149

145150
assert np.allclose(my_hess, tr_hess)
146151

147152

148153
def test_wrong_data(wrong_data, var_cov0, var_cov1):
149154
with pytest.raises(ValueError):
150-
PoissonModel(wrong_data, param_specs={"lam": {"variables": [var_cov0, var_cov1]}})
155+
create_poisson_model(
156+
wrong_data, param_specs={"lam": {"variables": [var_cov0, var_cov1]}}
157+
)
151158

152159

153160
def test_get_ui(model):
@@ -160,16 +167,18 @@ def test_get_ui(model):
160167

161168
def test_model_no_variables():
162169
num_obs = 5
163-
df = pd.DataFrame({
164-
"obs": np.random.rand(num_obs)*10,
165-
"offset": np.ones(num_obs),
166-
})
170+
df = pd.DataFrame(
171+
{
172+
"obs": np.random.rand(num_obs) * 10,
173+
"offset": np.ones(num_obs),
174+
}
175+
)
167176
data = Data(
168177
col_obs="obs",
169178
col_offset="offset",
170179
df=df,
171180
)
172-
model = PoissonModel(data, param_specs={"lam": {"offset": "offset"}})
181+
model = create_poisson_model(data, param_specs={"lam": {"offset": "offset"}})
173182
coefs = np.array([])
174183
grad = model.gradient(coefs)
175184
hessian = model.hessian(coefs)

0 commit comments

Comments
 (0)