Ackb ma rację, że tych rozwiązań opartych na wektorach nie można uznać za prawdziwe średnie kątów, są one jedynie średnią odpowiedników wektorów jednostkowych. Jednak rozwiązanie sugerowane przez ACKB nie wydaje się matematycznie poprawne.
Poniższe rozwiązanie jest matematycznie wyprowadzone z celu minimalizacji (kąt [i] - avgAngle) ^ 2 (gdzie różnica jest korygowana, jeśli to konieczne), co czyni go prawdziwą średnią arytmetyczną kątów.
Po pierwsze, musimy dokładnie przyjrzeć się, w których przypadkach różnica między kątami jest inna niż różnica między ich odpowiednikami z normalną liczbą. Rozważ kąty x i y, jeśli y> = x - 180 i y <= x + 180, to możemy bezpośrednio użyć różnicy (xy). W przeciwnym razie, jeśli pierwszy warunek nie zostanie spełniony, w obliczeniach musimy użyć (y + 360) zamiast y. Odpowiednio, jeśli drugi warunek nie jest spełniony, musimy użyć (y-360) zamiast y. Ponieważ równanie krzywej minimalizujemy tylko zmiany w punktach, w których te nierówności zmieniają się z prawdziwego na fałszywy lub odwrotnie, możemy rozdzielić pełny [0,360) zakres na zestaw segmentów oddzielonych tymi punktami. Następnie musimy tylko znaleźć minimum każdego z tych segmentów, a następnie minimum minimum każdego segmentu, które jest średnią.
Oto obraz pokazujący, gdzie występują problemy przy obliczaniu różnic kątów. Jeśli x znajduje się w szarym obszarze, wystąpi problem.
Aby zminimalizować zmienną, w zależności od krzywej, możemy wziąć pochodną tego, co chcemy zminimalizować, a następnie znaleźć punkt zwrotny (gdzie pochodna = 0).
Tutaj zastosujemy ideę zminimalizowania kwadratowej różnicy w celu wyprowadzenia wspólnego wzoru na średnią arytmetyczną: suma (a [i]) / n. Krzywą y = sum ((a [i] -x) ^ 2) można zminimalizować w ten sposób:
y = sum((a[i]-x)^2)
= sum(a[i]^2 - 2*a[i]*x + x^2)
= sum(a[i]^2) - 2*x*sum(a[i]) + n*x^2
dy\dx = -2*sum(a[i]) + 2*n*x
for dy/dx = 0:
-2*sum(a[i]) + 2*n*x = 0
-> n*x = sum(a[i])
-> x = sum(a[i])/n
Teraz zastosuj to do krzywych z naszymi dostosowanymi różnicami:
b = podzbiór a gdzie poprawna (kątowa) różnica a [i] -xc = podzbiór a gdzie poprawna (kątowa) różnica (a [i] -360) -x cn = wielkość cd = podzbiór a gdzie poprawna (kątowa) różnica (a [i] +360) -x dn = rozmiar d
y = sum((b[i]-x)^2) + sum(((c[i]-360)-b)^2) + sum(((d[i]+360)-c)^2)
= sum(b[i]^2 - 2*b[i]*x + x^2)
+ sum((c[i]-360)^2 - 2*(c[i]-360)*x + x^2)
+ sum((d[i]+360)^2 - 2*(d[i]+360)*x + x^2)
= sum(b[i]^2) - 2*x*sum(b[i])
+ sum((c[i]-360)^2) - 2*x*(sum(c[i]) - 360*cn)
+ sum((d[i]+360)^2) - 2*x*(sum(d[i]) + 360*dn)
+ n*x^2
= sum(b[i]^2) + sum((c[i]-360)^2) + sum((d[i]+360)^2)
- 2*x*(sum(b[i]) + sum(c[i]) + sum(d[i]))
- 2*x*(360*dn - 360*cn)
+ n*x^2
= sum(b[i]^2) + sum((c[i]-360)^2) + sum((d[i]+360)^2)
- 2*x*sum(x[i])
- 2*x*360*(dn - cn)
+ n*x^2
dy/dx = 2*n*x - 2*sum(x[i]) - 2*360*(dn - cn)
for dy/dx = 0:
2*n*x - 2*sum(x[i]) - 2*360*(dn - cn) = 0
n*x = sum(x[i]) + 360*(dn - cn)
x = (sum(x[i]) + 360*(dn - cn))/n
Samo to nie wystarczy, aby uzyskać minimum, podczas gdy działa dla normalnych wartości, które mają nieograniczony zbiór, więc wynik z pewnością będzie mieścił się w zakresie zestawu i dlatego jest ważny. Potrzebujemy minimum mieszczącego się w zakresie (określonym przez segment). Jeśli minimum jest mniejsze niż dolna granica naszego segmentu, to minimum tego segmentu musi znajdować się w dolnej granicy (ponieważ krzywe kwadratowe mają tylko 1 punkt zwrotny), a jeśli minimum jest większe niż górna granica naszego segmentu, to minimum segmentu jest na Górna granica. Gdy mamy minimum dla każdego segmentu, po prostu znajdujemy ten, który ma najniższą wartość tego, co minimalizujemy (suma ((b [i] -x) ^ 2) + suma (((c [i] -360 ) -b) ^ 2) + suma (((d [i] +360) -c) ^ 2)).
Oto obraz krzywej, który pokazuje, jak zmienia się w punktach, w których x = (a [i] +180)% 360. Zbiór danych, o którym mowa, to {65,92,230,320,250}.
Oto implementacja algorytmu w Javie, w tym kilka optymalizacji, jego złożoność wynosi O (nlogn). Można go zredukować do O (n), jeśli zastąpisz sortowanie oparte na porównaniu sortowaniem bez porównania, takim jak sortowanie radix.
static double varnc(double _mean, int _n, double _sumX, double _sumSqrX)
{
return _mean*(_n*_mean - 2*_sumX) + _sumSqrX;
}
//with lower correction
static double varlc(double _mean, int _n, double _sumX, double _sumSqrX, int _nc, double _sumC)
{
return _mean*(_n*_mean - 2*_sumX) + _sumSqrX
+ 2*360*_sumC + _nc*(-2*360*_mean + 360*360);
}
//with upper correction
static double varuc(double _mean, int _n, double _sumX, double _sumSqrX, int _nc, double _sumC)
{
return _mean*(_n*_mean - 2*_sumX) + _sumSqrX
- 2*360*_sumC + _nc*(2*360*_mean + 360*360);
}
static double[] averageAngles(double[] _angles)
{
double sumAngles;
double sumSqrAngles;
double[] lowerAngles;
double[] upperAngles;
{
List<Double> lowerAngles_ = new LinkedList<Double>();
List<Double> upperAngles_ = new LinkedList<Double>();
sumAngles = 0;
sumSqrAngles = 0;
for(double angle : _angles)
{
sumAngles += angle;
sumSqrAngles += angle*angle;
if(angle < 180)
lowerAngles_.add(angle);
else if(angle > 180)
upperAngles_.add(angle);
}
Collections.sort(lowerAngles_);
Collections.sort(upperAngles_,Collections.reverseOrder());
lowerAngles = new double[lowerAngles_.size()];
Iterator<Double> lowerAnglesIter = lowerAngles_.iterator();
for(int i = 0; i < lowerAngles_.size(); i++)
lowerAngles[i] = lowerAnglesIter.next();
upperAngles = new double[upperAngles_.size()];
Iterator<Double> upperAnglesIter = upperAngles_.iterator();
for(int i = 0; i < upperAngles_.size(); i++)
upperAngles[i] = upperAnglesIter.next();
}
List<Double> averageAngles = new LinkedList<Double>();
averageAngles.add(180d);
double variance = varnc(180,_angles.length,sumAngles,sumSqrAngles);
double lowerBound = 180;
double sumLC = 0;
for(int i = 0; i < lowerAngles.length; i++)
{
//get average for a segment based on minimum
double testAverageAngle = (sumAngles + 360*i)/_angles.length;
//minimum is outside segment range (therefore not directly relevant)
//since it is greater than lowerAngles[i], the minimum for the segment
//must lie on the boundary lowerAngles[i]
if(testAverageAngle > lowerAngles[i]+180)
testAverageAngle = lowerAngles[i];
if(testAverageAngle > lowerBound)
{
double testVariance = varlc(testAverageAngle,_angles.length,sumAngles,sumSqrAngles,i,sumLC);
if(testVariance < variance)
{
averageAngles.clear();
averageAngles.add(testAverageAngle);
variance = testVariance;
}
else if(testVariance == variance)
averageAngles.add(testAverageAngle);
}
lowerBound = lowerAngles[i];
sumLC += lowerAngles[i];
}
//Test last segment
{
//get average for a segment based on minimum
double testAverageAngle = (sumAngles + 360*lowerAngles.length)/_angles.length;
//minimum is inside segment range
//we will test average 0 (360) later
if(testAverageAngle < 360 && testAverageAngle > lowerBound)
{
double testVariance = varlc(testAverageAngle,_angles.length,sumAngles,sumSqrAngles,lowerAngles.length,sumLC);
if(testVariance < variance)
{
averageAngles.clear();
averageAngles.add(testAverageAngle);
variance = testVariance;
}
else if(testVariance == variance)
averageAngles.add(testAverageAngle);
}
}
double upperBound = 180;
double sumUC = 0;
for(int i = 0; i < upperAngles.length; i++)
{
//get average for a segment based on minimum
double testAverageAngle = (sumAngles - 360*i)/_angles.length;
//minimum is outside segment range (therefore not directly relevant)
//since it is greater than lowerAngles[i], the minimum for the segment
//must lie on the boundary lowerAngles[i]
if(testAverageAngle < upperAngles[i]-180)
testAverageAngle = upperAngles[i];
if(testAverageAngle < upperBound)
{
double testVariance = varuc(testAverageAngle,_angles.length,sumAngles,sumSqrAngles,i,sumUC);
if(testVariance < variance)
{
averageAngles.clear();
averageAngles.add(testAverageAngle);
variance = testVariance;
}
else if(testVariance == variance)
averageAngles.add(testAverageAngle);
}
upperBound = upperAngles[i];
sumUC += upperBound;
}
//Test last segment
{
//get average for a segment based on minimum
double testAverageAngle = (sumAngles - 360*upperAngles.length)/_angles.length;
//minimum is inside segment range
//we test average 0 (360) now
if(testAverageAngle < 0)
testAverageAngle = 0;
if(testAverageAngle < upperBound)
{
double testVariance = varuc(testAverageAngle,_angles.length,sumAngles,sumSqrAngles,upperAngles.length,sumUC);
if(testVariance < variance)
{
averageAngles.clear();
averageAngles.add(testAverageAngle);
variance = testVariance;
}
else if(testVariance == variance)
averageAngles.add(testAverageAngle);
}
}
double[] averageAngles_ = new double[averageAngles.size()];
Iterator<Double> averageAnglesIter = averageAngles.iterator();
for(int i = 0; i < averageAngles_.length; i++)
averageAngles_[i] = averageAnglesIter.next();
return averageAngles_;
}
Średnia arytmetyczna zbioru kątów może nie zgadzać się z intuicyjnym wyobrażeniem o tym, jaka powinna być średnia. Na przykład średnia arytmetyczna zbioru {179,179,0,181,181} wynosi 216 (i 144). Odpowiedź, o której natychmiast pomyślisz, to prawdopodobnie 180, jednak dobrze wiadomo, że na średnią arytmetyczną duży wpływ mają wartości krawędzi. Należy również pamiętać, że kąty nie są wektorami, tak atrakcyjne, jak może się to czasami wydawać, gdy mamy do czynienia z kątami.
Algorytm ten oczywiście ma również zastosowanie do wszystkich wielkości, które są zgodne z arytmetyką modularną (z minimalną korektą), takich jak pora dnia.
Chciałbym również podkreślić, że chociaż jest to prawdziwa średnia kątów, w przeciwieństwie do rozwiązań wektorowych, to niekoniecznie oznacza, że jest to rozwiązanie, którego powinieneś użyć, średnia odpowiednich wektorów jednostkowych może być wartością, którą faktycznie używasz powinien być używany.