Ackb правильно, що ці векторні рішення не можна вважати справжніми середніми кутами, вони є лише середнім рівнем одиничних векторів. Однак запропоноване рішення Ackb математично не здається.
Далі йде рішення, яке математично виводиться з мети мінімізації (кут [i] - avgAngle) ^ 2 (де різниця коригується при необхідності), що робить його справжнім середнім арифметичним кутом.
Спочатку нам потрібно розібратися, у яких випадках різниця між кутами відрізняється від різниці між їх звичайними числами. Розглянемо кути x і y, якщо y> = x - 180 і y <= x + 180, то ми можемо використовувати різницю (xy) безпосередньо. В іншому випадку, якщо перша умова не виконується, тоді ми повинні використовувати (y + 360) в обчисленні замість y. Відповідно, якщо друга умова не виконується, тоді ми повинні використовувати (y-360) замість y. Оскільки рівняння кривої ми мінімізуємо лише зміни в точках, де ці нерівності змінюються від істинних до хибних або навпаки, ми можемо розділити повний [0,360) діапазон на набір відрізків, розділених цими точками. Тоді нам потрібно лише знайти мінімум кожного з цих сегментів, а потім мінімум мінімуму кожного сегмента, який є середнім.
Ось зображення, що демонструє, де виникають проблеми при обчисленні різниці кутів. Якщо х лежить у сірій області, то виникне проблема.
Щоб мінімізувати змінну, залежно від кривої, ми можемо взяти похідну, яку ми хочемо мінімізувати, і тоді ми знайдемо точку повороту (де де похідна = 0).
Тут ми застосуємо ідею мінімізації різниці у квадраті для отримання загальної середньої арифметичної формули: sum (a [i]) / n. Криву y = sum ((a [i] -x) ^ 2) можна мінімізувати таким чином:
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
Тепер застосуємо його до кривих з нашими відрегульованими відмінностями:
b = підмножина a, де правильна (кутова) різниця a [i] -xc = підмножина a, де правильна (кутова) різниця (a [i] -360) -x cn = розмір cd = підмножина a, де правильна (кутова) різниця (a [i] +360) -x dn = розмір 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
Одного лише цього недостатньо, щоб отримати мінімум, хоча він працює для нормальних значень, який має необмежений набір, тому результат, безумовно, лежить у межах заданого діапазону і тому є дійсним. Нам потрібен мінімум у межах діапазону (визначеного сегментом). Якщо мінімум менший від нижньої межі нашого сегмента, то мінімум цього сегмента повинен бути на нижній межі (оскільки квадратичні криві мають лише 1 поворот), а якщо мінімум перевищує верхню межу нашого сегмента, то мінімум сегмента знаходиться на верхня межа. Після того, як у нас є мінімум для кожного сегмента, ми просто знаходимо те, що має найменше значення для того, що ми мінімізуємо (sum ((b [i] -x) ^ 2) + sum ((((c [i] -360) ) -b) ^ 2) + сума (((d [i] +360) -c) ^ 2)).
Ось зображення кривої, яке показує, як вона змінюється в точках, де x = (a [i] +180)% 360. Набір даних, про який йдеться, дорівнює {65,92,230,320,250}.
Ось реалізація алгоритму на Java, включаючи деякі оптимізації, його складність становить O (nlogn). Він може бути зменшений до O (n), якщо ви заміните сортування на основі порівняння на сортування, яке не базується на порівнянні, наприклад, сортування 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_;
}
Середнє арифметичне набору кутів може не узгоджуватися з вашим інтуїтивним уявленням про те, яким повинен бути середній показник. Наприклад, середнє арифметичне множини {179,179,0,181,181} дорівнює 216 (і 144). Відповідь, яку ви відразу ж придумаєте, - це, ймовірно, 180, проте добре відомо, що середнє арифметичне сильно впливає на крайові значення. Ви також повинні пам’ятати, що кути не є векторами, настільки привабливими, як це може здатися при роботі з кутами іноді.
Цей алгоритм, звичайно, також застосовується до всіх величин, які підкоряються модульній арифметиці (з мінімальним регулюванням), наприклад, час доби.
Я також хотів би підкреслити, що, хоча це справжнє середнє значення кутів, на відміну від векторних рішень, це не обов’язково означає, що це рішення, яке ви повинні використовувати, середнє значення відповідних одиничних векторів цілком може бути значенням, яке ви насправді слід використовувати.