Averages of Angles

15

1

Story, or why we are doing this.

None. This exercise is completely pointless ... unless you are Stephen Hawking.

The Challenge

Given a list of angles, find the average of those angles. For example the average of 91 degrees and -91 degrees is 180 degrees. You can use a program or function to do this.

Input

A list of degree values representing angle measures. You may assume that they will be integers. They can be inputted in any convenient format or provided as function arguments.

Output

The average of the inputted values. If there are more than one value found for the average, only one should be outputted. The average is defined as the value for which

enter image description here

is minimized. The output must be within the range of (-180, 180] and be accurate to at least two places behind the decimal point.

Examples:

> 1 3
2
> 90 -90
0 or 180
> 0 -120 120
0 or -120 or 120
> 0 810
45
> 1 3 3
2.33
> 180 60 -60
180 or 60 or -60
> 0 15 45 460
40
> 91 -91
180
> -89 89
0

As usual with , the submission with the least bytes wins.

Leaderboard

Here is a Stack Snippet to generate both a regular leaderboard and an overview of winners by language.

var QUESTION_ID=60538,OVERRIDE_USER=32700;function answersUrl(e){return"http://api.stackexchange.com/2.2/questions/"+QUESTION_ID+"/answers?page="+e+"&pagesize=100&order=desc&sort=creation&site=codegolf&filter="+ANSWER_FILTER}function commentUrl(e,s){return"http://api.stackexchange.com/2.2/answers/"+s.join(";")+"/comments?page="+e+"&pagesize=100&order=desc&sort=creation&site=codegolf&filter="+COMMENT_FILTER}function getAnswers(){jQuery.ajax({url:answersUrl(answer_page++),method:"get",dataType:"jsonp",crossDomain:!0,success:function(e){answers.push.apply(answers,e.items),answers_hash=[],answer_ids=[],e.items.forEach(function(e){e.comments=[];var s=+e.share_link.match(/\d+/);answer_ids.push(s),answers_hash[s]=e}),e.has_more||(more_answers=!1),comment_page=1,getComments()}})}function getComments(){jQuery.ajax({url:commentUrl(comment_page++,answer_ids),method:"get",dataType:"jsonp",crossDomain:!0,success:function(e){e.items.forEach(function(e){e.owner.user_id===OVERRIDE_USER&&answers_hash[e.post_id].comments.push(e)}),e.has_more?getComments():more_answers?getAnswers():process()}})}function getAuthorName(e){return e.owner.display_name}function process(){var e=[];answers.forEach(function(s){var r=s.body;s.comments.forEach(function(e){OVERRIDE_REG.test(e.body)&&(r="<h1>"+e.body.replace(OVERRIDE_REG,"")+"</h1>")});var a=r.match(SCORE_REG);a&&e.push({user:getAuthorName(s),size:+a[2],language:a[1],link:s.share_link})}),e.sort(function(e,s){var r=e.size,a=s.size;return r-a});var s={},r=1,a=null,n=1;e.forEach(function(e){e.size!=a&&(n=r),a=e.size,++r;var t=jQuery("#answer-template").html();t=t.replace("{{PLACE}}",n+".").replace("{{NAME}}",e.user).replace("{{LANGUAGE}}",e.language).replace("{{SIZE}}",e.size).replace("{{LINK}}",e.link),t=jQuery(t),jQuery("#answers").append(t);var o=e.language;/<a/.test(o)&&(o=jQuery(o).text()),s[o]=s[o]||{lang:e.language,user:e.user,size:e.size,link:e.link}});var t=[];for(var o in s)s.hasOwnProperty(o)&&t.push(s[o]);t.sort(function(e,s){return e.lang>s.lang?1:e.lang<s.lang?-1:0});for(var c=0;c<t.length;++c){var i=jQuery("#language-template").html(),o=t[c];i=i.replace("{{LANGUAGE}}",o.lang).replace("{{NAME}}",o.user).replace("{{SIZE}}",o.size).replace("{{LINK}}",o.link),i=jQuery(i),jQuery("#languages").append(i)}}var ANSWER_FILTER="!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe",COMMENT_FILTER="!)Q2B_A2kjfAiU78X(md6BoYk",answers=[],answers_hash,answer_ids,answer_page=1,more_answers=!0,comment_page;getAnswers();var SCORE_REG=/<h\d>\s*([^\n,]*[^\s,]),.*?(\d+)(?=[^\n\d<>]*(?:<(?:s>[^\n<>]*<\/s>|[^\n<>]+>)[^\n\d<>]*)*<\/h\d>)/,OVERRIDE_REG=/^Override\s*header:\s*/i;
body{text-align:left!important}#answer-list,#language-list{padding:10px;width:290px;float:left}table thead{font-weight:700}table td{padding:5px}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/codegolf/all.css?v=83c949450c8b"> <div id="answer-list"> <h2>Leaderboard</h2> <table class="answer-list"> <thead> <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr></thead> <tbody id="answers"> </tbody> </table> </div><div id="language-list"> <h2>Winners by Language</h2> <table class="language-list"> <thead> <tr><td>Language</td><td>User</td><td>Score</td></tr></thead> <tbody id="languages"> </tbody> </table> </div><table style="display: none"> <tbody id="answer-template"> <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td>{{SIZE}}</td><td><a href="{{LINK}}">Link</a></td></tr></tbody> </table> <table style="display: none"> <tbody id="language-template"> <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td>{{SIZE}}</td><td><a href="{{LINK}}">Link</a></td></tr></tbody> </table>

To make sure that your answer shows up, please start your answer with a headline, using the following Markdown template:

## Language Name, N bytes

where N is the size of your submission. If you improve your score, you can keep old scores in the headline, by striking them through. For instance:

## Ruby, <s>104</s> <s>101</s> 96 bytes

If there you want to include multiple numbers in your header (e.g. because your score is the sum of two files or you want to list interpreter flag penalties separately), make sure that the actual score is the last number in the header:

## Perl, 43 + 2 (-p flag) = 45 bytes

You can also make the language name a link which will then show up in the leaderboard snippet:

## [><>](http://esolangs.org/wiki/Fish), 121 bytes

Here is a chatroom for any questions about the problem: http://chat.stackexchange.com/rooms/30175/room-for-average-of-angles

TheNumberOne

Posted 2015-10-12T20:12:26.323

Reputation: 10 855

Shouldn't 90,-90 give 180 if 91,-91 gives 180? – Blue – 2015-10-12T20:58:40.290

2Intuitively the average of -91 and 91 is 0, not 180. Using your definition we have: (180-91)^2+(180- -91)^2 => 81362, while (0-91)^2+(0- -91)^2 => 16562. So 180 surely can not be the average. What am I missing here? – edc65 – 2015-10-12T20:59:58.417

91%360=91;-91%360=269;(269+91)/2=180. Never mind, misread. Maybe? I'm not sure now. – Blue – 2015-10-12T21:01:31.667

Ok thanks. Still no idea about how finding it – edc65 – 2015-10-12T21:52:48.760

So the set 270, -90 should be -90, yes? – Addison Crump – 2015-10-12T22:07:30.050

And my answer also gets 0 for -91 and 91. I'll do some more work on it. – Addison Crump – 2015-10-12T22:10:34.757

So, by what I see here, do you assume that 360 and 0 average to 180? O.o – Addison Crump – 2015-10-12T22:16:02.870

Okay. I think I get it now, the angular distance kinda threw me off. I have an answer now. I hope. – Addison Crump – 2015-10-12T22:17:59.237

Tested and verified - It gets it. :) – Addison Crump – 2015-10-12T22:21:46.710

sanity check me: can this be solved by simply adding together unit vectors of the given angles, and printing the angle of the sum? – Sparr – 2015-10-12T23:13:50.987

I think your snippet is from another challenge. – cole – 2015-10-13T02:30:15.413

3None of your test cases so far break the incorrect algorithm of simply taking all the angles mod 360°, taking their average, and then subtracting 360° if the result is greater than 180°. You should add a case like [89°, −89°], which should return 0°. – Anders Kaseorg – 2015-10-13T04:25:12.360

Note for solvers. I believe that, instead of trying to cut the circle at every possible point, it should be sufficient to do so at each of the given points (but without, of course, omitting that point from your calculations!). This is because the average of all the points when the circle is cut at any point beteeen input1 and input2 (where the inputs are numbered in cyclic order) is I think the same as the average when the circle is cut at input2 (including input2). – msh210 – 2018-05-13T16:25:38.147

Answers

7

Python 3, 129 bytes

lambda l:min([sum(((b-a)%360)**2for b in l)*len(l)-s*s,180-(180-a-s/len(l))%360]for a in l for s in[sum((b-a)%360for b in l)])[1]

This problem seems to have generated quite a lot of confusion. Intuitively, the idea is to cut the circle of angles at some point, unwrap the circle to a line, compute the arithmetic mean on that line, and then wrap the result back to the circle. But there are many different points where you could choose to cut the circle. It is not enough to arbitrarily pick one, such as 0° or 180°. You need to try them all and see which one results in the smallest sum of squared distances. If your solution is significantly less complicated than this, it is probably wrong.

Anders Kaseorg

Posted 2015-10-12T20:12:26.323

Reputation: 29 242

1@AndreasKaseorg I think you can save one byte by changing s**2 to s*s – Ioannes – 2015-10-13T07:55:03.017

See my comment on the question. – msh210 – 2018-05-13T16:27:04.787

@msh210 Not sure why you’re directing this comment at me specifically. My solution already works that way. – Anders Kaseorg – 2018-05-13T17:27:36.693

It was partially in reply to the last sentence of this answer post. – msh210 – 2018-05-13T18:13:13.987

4

Python 3, 85 bytes

lambda l:180-min(range(72000),key=lambda x:sum((180-(x/200+i)%360)**2for i in l))/200

Takes advantage of the the answer only needing to be accurate to two decimal points by trying all possible angles with increments of 1/200 of a degree. This takes less than a second on my machine.

Because Python doesn't let us conveniently list arithmetic progressions of floats, we represent the possible angles as whole number [0,72000), which convert to an angle as (-180,180] as x -> 180 - x/200. We find the one of these that gives the minimum sum of squared angular differences.

For two angles with an angular displacement of d, the squared angular distance is found by transforming to an equivalent angle in (-180,180] as 180-(d+180)%360, then squaring. Conveniently, the angle given by x/200 is already offset by 180 degrees.

xnor

Posted 2015-10-12T20:12:26.323

Reputation: 115 687

Using increments of 1/200 is actually problematic. For the test case [1, 3, 3], this solution returns 2.335 and is rounded to 2.34 while the correct answer should be 2.33. – Joel – 2019-08-27T00:08:31.240

@Joel I'm not sure where you're getting the rounding from, it looks like the decimal digits 2.33 are right in that example. In any case, would changing the 200 to 400 or to 2000 (and 72000 correspondingly) make it work despite rounding? Also, looking at this old problem again, I think I might see a better way. – xnor – 2019-08-27T00:24:24.883

Here you may actually use an increment of $0.01$. This is because the target function to optimize is basically a piecewise quadratic function. So if the minimum $m=\mathrm{argmin}_x f(x)$ lies in $[s,s+0.01]$ and $f(s)<f(s+0.01)$, it is more or less guaranteed that $|m-s|<|m-s+0.01|$ and ${\rm round}(m)=s$ (I use "more or less" because I am not sure about the boundary effect for each segment of $f$). The same holds for $f(s)>f(s+0.01)$. In the case of a tie $f(s)=f(s+0.01)$, ${\rm round}(m)=s+0.01$. Note that for an arbitrary $f$ this property may not be guaranteed. – Joel – 2019-08-27T00:44:06.903

Here is a TIO link for you to test.

– Joel – 2019-08-27T00:55:21.833

Oh, I just realized that you are right. If the correct answer is 2.333... and your program returns 2.335, it is correct until two decimal places without rounding. Sorry for that. – Joel – 2019-08-27T00:59:51.973

3

Octave, 97 95 bytes

p=pi;x=p:-1e-5:-p;z=@(L)sum((p-abs(abs(x-mod(L,360)*p/180)-p)).^2);@(L)x(z(L)==min(z(L)))*180/p

This produces an anonymous function that just searches the minimum of the given function on a grid that is just fine enough. As input the function accepts column vectors, e.g. [180; 60; -60]. For testing you need to give the function a name. So you could e.g. run the code above and then use ans([180, 60; -60]).

flawr

Posted 2015-10-12T20:12:26.323

Reputation: 40 560

Yes, it returns 180. – flawr – 2015-10-16T15:13:07.343

2

Javascript ES6, 87 bytes

with(Math)f=(...n)=>(t=>180/PI*atan(t(sin)/t(cos)))(f=>n.reduce((p,c)=>p+=f(c*PI/180)))

Example runs (Tested in Firefox):

f(-91,91)     // -0
f(-90,90)     // 0
f(0,-120,120) // 0
f(0,810)      // 44.999999999999936

Work in progress

This version takes a slightly different approach than the average-everything-then-do-modular-math. Rather, the angles are converted to vectors, the vectors are added and the angle of the resulting vector is then computed. Unfortunately, this version is very unstable with the trig and I'll be working on a modular-math version.

Dendrobium

Posted 2015-10-12T20:12:26.323

Reputation: 2 412

1f(-91,91) should return 180. – TheNumberOne – 2015-10-13T00:08:32.347

1Even if it were implemented correctly, a vector addition approach cannot compute the specified result. Vector addition maximizes the sum of cosines of angular differences, rather than minimizing the sum of squares of angular differences. – Anders Kaseorg – 2015-10-13T03:48:50.523

2

CJam,  44  40 bytes

Ie3_2*,f-:e-2{ea:~f{-P*180/mcmC2#}:+}$0=

Try it online in the CJam interpreter.

Test cases

$ for i in 1\ 3 90\ -90 0\ -120\ 120 0\ 810 1\ 3\ 3 180\ 60\ -60 0\ 15\ 45\ 460 91\ -91 -89\ 89
> do cjam <(echo 'Ie3_2*,f-:e-2{ea:~f{-P*180/mcmC2#}:+}$0=') $i
> echo
> done
2.0
180.0
0.0
45.0
2.33
60.0
40.0
180.0
0.0

Idea

We compute the deviation for all potential averages from -179.99 to 180.00 with steps of size 0.01, and select the one with the lowest deviation.

For this purpose, it doesn't matter if we take the angular distances degrees or radians. Rather than mapping the differences δ of angles from input and potential averages in [0,360°) and conditionally subtracting the result from 180°, we can simply calculate arccos(cos(πδ&div;180°)), since cos is both periodic and even, and arccos always yields a value in [0,π).

Code

Ie3        e# Push 18e3 = 18,000.
_2*        e# Copy and multiply by 2. Pushes 36,000.
,          e# Push the range [0 ... 35,999].
f-         e# Subtract each element from 18,000. Pushes [18,000 ... -17,999].
:e-2       e# Divide each element by 100. Pushes [180.00 ... -179.99].
{          e# Sort; for each element A of that array:
  ea:~     e#   Push and evaluate the array of command-line arguments.
  f{       e#   For each input angle, push A and the angle; then:
    -      e#     Subtract the angle from A.
    P*180/ e#     Convert from degrees to radians.
    mcmC   e#     Apply cos, then arccos to the result.
    2#     e#     Square.
  }        e#
  :+       e#   Add the squares. This calculates the deviation.
}$         e# A's with lower deviations come first.
0=         e# Select the first element of the sorted array.

Dennis

Posted 2015-10-12T20:12:26.323

Reputation: 196 637

1

MATLAB, 151

p=360;n=mod(input(''),p);a=0:0.01:p;m=[];for b=a e=b-n;f=mod([e;-e],p);c=min(f);d=c.^2;m=[m sum(d)];end;[~,i]=min(m);a=a(i);if a>180 a=a-p;end;disp(a);

Ok, so until I can actually understand what the methodology is, this is what I have come up with. It is a little bit of a hack, but as the question states that the answer must be correct to 2.d.p it should work.

I basically check every angle between 0 and 360 (in 0.01 increments) and then solve the formula in the question for each of those angles. Then the angle with the smallest sum is picked and converted into -180 to 180 range.


The code should with Octave. You can try it with the online interpreter

Tom Carpenter

Posted 2015-10-12T20:12:26.323

Reputation: 3 990

1°, 183° should result in −88°, not 92°. – Anders Kaseorg – 2015-10-13T04:09:21.433

@AndersKaseorg try again now. – Tom Carpenter – 2015-10-13T04:25:15.757

Nope, never mind. Back to the drawing board again... – Tom Carpenter – 2015-10-13T04:27:11.613

1

JavaScript (ES6) 138

Not having the faintest idea of an algorithm, this tries all possibile values with 2 digits precision (-179.99 to 180.00). Quite fast with the test cases anyway.

Test running the snippet below in an EcmaScript 6 compliant browser (implementing arrow functions and default parameters - AFAIK Firefox)

A=l=>(D=(a,b,z=a>b?a-b:b-a)=>z>180?360-z:z,m=>{for(i=-18000;i++<18000;)l.some(v=>(t+=(d=D(v%360,i/100))*d)>m,t=0)||(m=t,r=i)})(1/0)||r/100

// Test
console.log=x=>O.innerHTML+=x+'\n'

;[[1,3],[89,-89],[90,-90],[91,-91],[0,120,-120],[0,810],[1,3,3],[180,60,-60],[0,15,45,460],[1,183]]
.forEach(t=>console.log(t+' -> '+A(t)))

// Less golfed

A=l=>{
  D=(a,b,z=a>b?a-b:b-a) => z>180?360-z:z; // angular distance
  m=1/0;
  for(i=-18000;i++<18000;) // try all from -179.99 to 180
  {
    t = 0;
    if (!l.some(v => (t+=(d=D(v%360,i/100))*d) > m))
    {
      m = t;
      r = i;
    }  
  }  
  return r/100;
}
<pre id=O></pre>

edc65

Posted 2015-10-12T20:12:26.323

Reputation: 31 086