Skip to content

Commit c86b842

Browse files
authored
Fix NaN handling bug in adjusted, unbiased ema_cov with a finite horizon and ignore_na=True (#550)
Signed-off-by: Adam Glustein <adam.glustein@point72.com>
1 parent 4c5a362 commit c86b842

2 files changed

Lines changed: 59 additions & 24 deletions

File tree

cpp/csp/cppnodes/statsimpl.h

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,38 +1597,43 @@ class AlphaDebiasEMA
15971597

15981598
AlphaDebiasEMA & operator=( AlphaDebiasEMA && rhs ) = default;
15991599

1600+
16001601
void add( double x )
16011602
{
1602-
if( m_first && likely( !isnan( x ) ) )
1603-
{
1604-
m_wsum = 1;
1605-
m_sqsum = 1;
1606-
m_first = false;
1607-
}
1608-
else if( likely( !isnan( x ) ) )
1603+
if( likely( !isnan( x ) ) )
16091604
{
1610-
double decay_factor = ( m_ignore_na ? m_decay : pow( m_decay, m_offset ) );
1611-
m_wsum *= decay_factor;
1612-
m_sqsum *= decay_factor * decay_factor;
1613-
m_offset = 1;
1614-
1615-
double w0;
1616-
if( m_adjust )
1617-
w0 = 1.0;
1605+
if( unlikely( m_first ) )
1606+
{
1607+
m_wsum = 1;
1608+
m_sqsum = 1;
1609+
m_first = false;
1610+
}
16181611
else
1619-
w0 = 1 - m_decay;
1620-
m_sqsum += w0 * w0;
1621-
m_wsum += w0;
1622-
if( !m_adjust )
16231612
{
1624-
double correction = decay_factor + w0;
1625-
m_wsum /= correction;
1626-
m_sqsum /= ( correction * correction );
1613+
double decay_factor = ( m_ignore_na ? m_decay : pow( m_decay, m_offset ) );
1614+
m_wsum *= decay_factor;
1615+
m_sqsum *= decay_factor * decay_factor;
1616+
m_offset = 1;
1617+
1618+
double w0;
1619+
if( m_adjust )
1620+
w0 = 1.0;
1621+
else
1622+
w0 = 1 - m_decay;
1623+
m_sqsum += w0 * w0;
1624+
m_wsum += w0;
1625+
if( !m_adjust )
1626+
{
1627+
double correction = decay_factor + w0;
1628+
m_wsum /= correction;
1629+
m_sqsum /= ( correction * correction );
1630+
}
16271631
}
16281632
}
1629-
else if ( likely( !m_first ) )
1633+
else
16301634
{
1631-
m_offset++;
1635+
if( likely( !m_first ) )
1636+
m_offset++;
16321637
m_nan_count++;
16331638
}
16341639
}

csp/tests/test_stats.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3640,6 +3640,36 @@ def g(allow_non_overlapping: bool):
36403640
# Additional sanity check is that correlation should be 1 as middle ticks are ignored
36413641
np.testing.assert_allclose(res["corr"][1], np.array([np.nan, 1.0, 1.0]), equal_nan=True)
36423642

3643+
def test_ema_cov_horizon_bug(self):
3644+
# Bug in finite horizon, adjusted, unbiased EMA covariance with ignore_na=True
3645+
# Also applies to ema_var/ema_std as well, as they use cov
3646+
# When the first data points are NaN, after the initial NaN is removed the next value that is removed has the wrong lookback weight applied to it
3647+
3648+
st = datetime(2020, 1, 1)
3649+
N = 15
3650+
K = 3
3651+
horizon = 10
3652+
alpha = 0.1
3653+
values = [np.nan if i < K else float(i) for i in range(N)]
3654+
3655+
@csp.graph
3656+
def g():
3657+
x = csp.curve(
3658+
typ=float, data=[(st + timedelta(seconds=i), values[i]) for i in range(N)]
3659+
) # start with some NaNs
3660+
ema_std = csp.stats.ema_std(x, alpha=alpha, adjust=True, bias=False, ignore_na=True, horizon=horizon)
3661+
csp.add_graph_output("ema_std", ema_std)
3662+
3663+
res = csp.run(g, starttime=st, endtime=timedelta(seconds=N), output_numpy=True)
3664+
3665+
golden_ema_std = np.array(
3666+
[
3667+
pd.Series(values[max(0, j - horizon + 1) : j + 1]).ewm(alpha=alpha, ignore_na=True).std().iloc[-1]
3668+
for j in range(N)
3669+
]
3670+
)
3671+
np.testing.assert_allclose(res["ema_std"][1], golden_ema_std, atol=1e-10)
3672+
36433673

36443674
if __name__ == "__main__":
36453675
unittest.main()

0 commit comments

Comments
 (0)