5

I have the following sample dataset:

In [222]: df
Out[222]:
    ci_low  circ_time_angle  ci_high
0       30               30       30
1       10                0       20
2     -188              143      207
3     -188                4      207
4     -188                8      207
5     -188               14      207
6     -188              327      207
7      242               57      474
8      242              283      474
9      242                4      474
10    -190              200       -1
11     -90              300        0
12     -25               15       60
13     -30              349      350
14     420               30      600
15    -100               23      719
16    -100               23      259
17    -350                5      -10
18    -350               11      -10

where:

  • ci_low - is a lower bound of a circular confidence interval (CI)
  • circ_time_angle - is an angle that I want to check whether it falls into CI or not
  • ci_high - is an upper bound of a circular confidence interval (CI)

Constraints:

  • 0 <= circ_time_angle <= 360
  • ci_high >= ci_low
  • ci_low and ci_high do NOT necessarily belong to [0, 360] (see rows [2-18] in the sample dataset).

Question: what would be the elegant way to check whether the angle circ_time_angle falls into circular confidence interval: [ci_low, ci_high]? Or do I need to check separately all the edge cases?

Desired / resulting dataset:

In [224]: res
Out[224]:
    ci_low  circ_time_angle  ci_high  falls_into_CI
0       30               30       30           True
1       10                0       20          False
2     -188              143      207           True
3     -188                4      207           True
4     -188                8      207           True
5     -188               14      207           True
6     -188              327      207           True
7      242               57      474           True
8      242              283      474           True
9      242                4      474           True
10    -190              200       -1           True
11     -90              300        0           True
12     -25               15       60           True
13     -30              349      350           True
14     420               30      600          False
15    -100               23      719           True
16    -100               23      259           True
17    -350                5      -10          False
18    -350               11      -10           True

I also tried to convert CI boundaries to [0, 360] and to [-180, 180], but it still didn't help me to find an elegant formula.


Sample dataset setup:

data = {
'ci_low': [30,
  10,
  -188,
  -188,
  -188,
  -188,
  -188,
  242,
  242,
  242,
  -190,
  -90,
  -25,
  -30,
  420,
  -100,
  -100,
  -350,
  -350],
 'circ_time_angle': [30,
  0,
  143,
  4,
  8,
  14,
  327,
  57,
  283,
  4,
  200,
  300,
  15,
  349,
  30,
  23,
  23,
  5,
  11],
 'ci_high': [30,
  20,
  207,
  207,
  207,
  207,
  207,
  474,
  474,
  474,
  -1,
  0,
  60,
  350,
  600,
  719,
  259,
  -10,
  -10]}
  
df = pd.DataFrame(data)
MaxU
  • 173,524
  • 24
  • 290
  • 329
  • You could try to not use eval :) `df['falls_in_CI'] = df['ci_low'] <= df['circ_time_angle'] <= df['ci_high']` – Joe Jun 26 '19 at 11:29
  • 1
    There is a lot of code out there so solve this, https://stackoverflow.com/questions/1878907/the-smallest-difference-between-2-angles – Joe Jun 26 '19 at 11:32
  • 1
    https://stackoverflow.com/questions/7570808/how-do-i-calculate-the-difference-of-two-angle-measures – Joe Jun 26 '19 at 11:32
  • @Joe, thank you for the links! I'm checking them now... – MaxU Jun 26 '19 at 11:53

2 Answers2

1

Currently I've come up with the following idea:

  1. check those CI intervals covering 360+ degrees and set result to True for the corresponding rows
  2. for all other rows, rotate all angles (columns: ['ci_low', 'circ_time_angle', 'ci_high']) in the way that ci_low == 0 and compare rotated circ_time_angle % 360 <= ci_high % 360

Code:

def angle_falls_into_interval(angle, lower, upper, high=360):
    # rotate ALL angles in the way, 
    # so that the lower angle = 0 degrees / radians.
    lower = np.asarray(lower)
    angle = np.asarray(angle) - lower
    upper = np.asarray(upper) - lower
    lower -= lower
    return np.where(upper-lower >= high, 
                    True, 
                    (angle % high) <= (upper % high))

Check:

In [232]: res = df.assign(falls_into_CI=angle_falls_into_interval(df.circ_time_angle, 
                                                                  df.ci_low, 
                                                                  df.ci_high,
                                                                  high=360))

In [233]: res
Out[233]:
    ci_low  circ_time_angle  ci_high  falls_into_CI
0       30               30       30           True
1       10                0       20          False
2     -188              143      207           True
3     -188                4      207           True
4     -188                8      207           True
5     -188               14      207           True
6     -188              327      207           True
7      242               57      474           True
8      242              283      474           True
9      242                4      474           True
10    -190              200       -1           True
11     -90              300        0           True
12     -25               15       60           True
13     -30              349      350           True
14     420               30      600          False
15    -100               23      719           True
16    -100               23      259           True
17    -350                5      -10          False
18    -350               11      -10           True
MaxU
  • 173,524
  • 24
  • 290
  • 329
1

I would try to normalize ci_low in the [0:360) range, and change ci_high by the same value. Then I would add 360 to circ_time_angle if it is below ci_low.

After that, the condition to be inside the CI interval is just circ_time_angle<ci_high.

I used an auxilliary dataframe to prevent any change in df:

limits = df[['ci_low', 'ci_high']].copy()   # copy ci_low and ci_high
limits.columns=['low', 'high']              # rename to have shorter names
# ensure ci_low is in the [0-360) range
delta = (limits['low'] // 360) * 360
limits['low'] -= delta
limits['high'] -= delta
limits['circ'] = df['circ_time_angle']      # copy circ_time_angle
# add 360 to circ_time_angle if it is below low
limits.loc[limits.circ < limits.low, 'circ'] += 360
df['falls_into_CI'] = limits['circ']<=limits['high']

It gives as expected:

    ci_low  circ_time_angle  ci_high  falls_into_CI
0       30               30       30           True
1       10                0       20          False
2     -188              143      207           True
3     -188                4      207           True
4     -188                8      207           True
5     -188               14      207           True
6     -188              327      207           True
7      242               57      474           True
8      242              283      474           True
9      242                4      474           True
10    -190              200       -1           True
11     -90              300        0           True
12     -25               15       60           True
13     -30              349      350           True
14     420               30      600          False
15    -100               23      719           True
16    -100               23      259           True
17    -350                5      -10          False
18    -350               11      -10           True

The nice point here if that everything is vectorized.

Serge Ballesta
  • 121,548
  • 10
  • 94
  • 199
  • Thank you for your answer! I think this is not quite correct... My goal is to check whether an angle falls into the specified confidence interval. If the confidence interval is `[-188, 207]`, then this CI covers the whole circle (`207 - -188 == 395`) and thus the result must be `True`. The same applies to `[-100, 719]`. – MaxU Jun 26 '19 at 14:49
  • 1
    @MaxU: Ok, I will have to slightly change my approach then. – Serge Ballesta Jun 26 '19 at 14:53
  • yep, it looks good now. :) Your idea is similar to one I have used in my answer ;) – MaxU Jun 26 '19 at 15:07