Skip to content

Commit

Permalink
Codecov (#628)
Browse files Browse the repository at this point in the history
* exclude dgps and exceptions from codecov

* upload tests for merge on master to codecov

* add token

* add tests for event_study

* dont test confit for twfe

* add type checks tests for event_study
  • Loading branch information
s3alfisc authored Sep 22, 2024
1 parent 44bed05 commit 5bf8cc9
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ jobs:
- name: Run tests
run: |
poetry run pytest tests -m slow --cov=./ --cov-report=xml
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

test_notebooks:
name: "Test Notebooks"
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/extended-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: Rscript -e 'install.packages(c("fixest", "broom", "did2s", "clubSandwich", "wildrwolf", "ivDiag"))'
shell: bash

# Run tests for PRs with the label "plots"
# Run tests for PRs with the label "extended"
- name: Run tests for plots (only on PRs with the 'tests-extended' label)
if: github.event_name == 'pull_request' && contains(github.event.label.name, 'tests-extended')
run: poetry run pytest tests -m "extended" --cov=pyfixest --cov-report=xml
Expand All @@ -59,3 +59,7 @@ jobs:
- name: Run tests for push to master
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
run: poetry run pytest tests -m "extended" --cov=pyfixest --cov-report=xml

# upload to codecov
- name: Upload to Codecov
uses: codecov/codecov-action@v4
3 changes: 3 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ coverage:
if_ci_failed: error
informational: false
only_pulls: false
ignore:
- "pyfixest/utils/dgps.py"
- "pyfixest/utils/_exceptions.py"
1 change: 0 additions & 1 deletion pyfixest/did/estimation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from pyfixest.did.did2s import DID2S, _did2s_estimate, _did2s_vcov
from pyfixest.did.lpdid import LPDID
from pyfixest.did.twfe import TWFE
from pyfixest.errors import NotImplementedError


def event_study(
Expand Down
8 changes: 4 additions & 4 deletions pyfixest/estimation/feols_.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,12 +822,12 @@ def get_inference(self, alpha: float = 0.05) -> None:
df = _N - _k if _vcov_type in ["iid", "hetero"] else _G - 1

# use t-dist for linear models, but normal for non-linear models
if _method == "feols":
self._pvalue = 2 * (1 - t.cdf(np.abs(self._tstat), df))
z = np.abs(t.ppf(alpha / 2, df))
else:
if _method == "fepois":
self._pvalue = 2 * (1 - norm.cdf(np.abs(self._tstat)))
z = np.abs(norm.ppf(alpha / 2))
else:
self._pvalue = 2 * (1 - t.cdf(np.abs(self._tstat), df))
z = np.abs(t.ppf(alpha / 2, df))

z_se = z * self._se
self._conf_int = np.array([_beta_hat - z_se, _beta_hat + z_se])
Expand Down
228 changes: 228 additions & 0 deletions tests/test_event_study.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import numpy as np
import pandas as pd
import pytest

import pyfixest as pf
from pyfixest.did.estimation import did2s, event_study


@pytest.fixture
def data():
df_het = pd.read_csv("pyfixest/did/data/df_het.csv")
return df_het


def test_event_study_twfe(data):
twfe = event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
att=True,
estimator="twfe",
)

twfe_feols = pf.feols("dep_var ~ treat | state + year", data=data)

assert np.allclose(
twfe.coef().values, twfe_feols.coef().values
), "TWFE coefficients are not the same."
assert np.allclose(
twfe.se().values, twfe_feols.se().values
), "TWFE standard errors are not the same."
assert np.allclose(
twfe.pvalue().values, twfe_feols.pvalue().values
), "TWFE p-values are not the same."

# TODO - minor difference, likely due to how z statistic is
# calculated

# assert np.allclose(
# twfe.confint().values, twfe_feols.confint().values
# ), "TWFE confidence intervals are not the same."


def test_event_study_did2s(data):
event_study_did2s = event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
att=True,
estimator="did2s",
)

fit_did2s = did2s(
data=data,
yname="dep_var",
first_stage="~ 0 | state + year",
second_stage="~treat",
treatment="treat",
cluster="state",
)

assert np.allclose(
event_study_did2s.coef().values, fit_did2s.coef().values
), "DID2S coefficients are not the same."
assert np.allclose(
event_study_did2s.se().values, fit_did2s.se().values
), "DID2S standard errors are not the same."
assert np.allclose(
event_study_did2s.pvalue().values, fit_did2s.pvalue().values
), "DID2S p-values are not the same."
assert np.allclose(
event_study_did2s.confint().values, fit_did2s.confint().values
), "DID2S confidence intervals are not the same."


# ---------------------------------------------------------------------------------
# test errors


# Test case for 'data' must be a pandas DataFrame
def test_event_study_invalid_data_type(data):
with pytest.raises(AssertionError, match="data must be a pandas DataFrame"):
event_study(
data="invalid_data", # Invalid data type, should be pd.DataFrame
yname="dep_var",
idname="state",
tname="year",
gname="g",
estimator="twfe",
)


# Test case for 'yname' must be a string
def test_event_study_invalid_yname_type(data):
with pytest.raises(AssertionError, match="yname must be a string"):
event_study(
data=data,
yname=123, # Invalid yname type, should be str
idname="state",
tname="year",
gname="g",
estimator="twfe",
)


# Test case for 'idname' must be a string
def test_event_study_invalid_idname_type(data):
with pytest.raises(AssertionError, match="idname must be a string"):
event_study(
data=data,
yname="dep_var",
idname=123, # Invalid idname type, should be str
tname="year",
gname="g",
estimator="twfe",
)


# Test case for 'tname' must be a string
def test_event_study_invalid_tname_type(data):
with pytest.raises(AssertionError, match="tname must be a string"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname=2020, # Invalid tname type, should be str
gname="g",
estimator="twfe",
)


# Test case for 'gname' must be a string
def test_event_study_invalid_gname_type(data):
with pytest.raises(AssertionError, match="gname must be a string"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname=2020, # Invalid gname type, should be str
estimator="twfe",
)


# Test case for 'xfml' must be a string or None
def test_event_study_invalid_xfml_type(data):
with pytest.raises(AssertionError, match="xfml must be a string or None"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
xfml=123, # Invalid xfml type, should be str or None
estimator="twfe",
)


# Test case for 'estimator' must be a string
def test_event_study_invalid_estimator_type(data):
with pytest.raises(AssertionError, match="estimator must be a string"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
estimator=123, # Invalid estimator type, should be str
)


# Test case for 'att' must be a boolean
def test_event_study_invalid_att_type(data):
with pytest.raises(AssertionError, match="att must be a boolean"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
att="True", # Invalid att type, should be bool
estimator="twfe",
)


# Test case for 'cluster' must be a string
def test_event_study_invalid_cluster_type(data):
with pytest.raises(AssertionError, match="cluster must be a string"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
estimator="twfe",
cluster=123, # Invalid cluster type, should be str
)


# Test case for 'cluster' must be 'idname'
def test_event_study_invalid_cluster_value(data):
with pytest.raises(AssertionError, match="cluster must be idname"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
estimator="twfe",
cluster="invalid_cluster", # Invalid cluster value
)


# Test case for unsupported estimator (triggering NotImplementedError)
def test_event_study_unsupported_estimator(data):
with pytest.raises(NotImplementedError, match="Estimator not supported"):
event_study(
data=data,
yname="dep_var",
idname="state",
tname="year",
gname="g",
estimator="unsupported", # Unsupported estimator
)

0 comments on commit 5bf8cc9

Please sign in to comment.