Skip to content

Commit aea2df3

Browse files
AccuracyCalculator
1 parent 5c4686f commit aea2df3

2 files changed

Lines changed: 180 additions & 0 deletions

File tree

src/iplane/accuracy_calculator.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'''Calculates accuracy statistics for fractional errors.'''
2+
3+
from collections import namedtuple
4+
import pandas as pd # type: ignore
5+
import numpy as np
6+
import matplotlib.pyplot as plt
7+
from matplotlib.axes import Axes
8+
from typing import List, Optional, cast, Union
9+
10+
PERCENTILE_STATISTICS = [0.9, 0.99]
11+
COL_CDF = 'cdf'
12+
COL_ERROR = 'error'
13+
14+
15+
AccuracyResult = namedtuple('AccuracyResult', ['accuracy', 'mean_absolute_error', 'cdf_df'])
16+
17+
18+
class AccuracyCalculator(object):
19+
"""Calculates accuracy statistics for fractional errors."""
20+
21+
def __init__(self, error_arr: np.ndarray):
22+
"""
23+
Args:
24+
error_arr (np.ndarray): Array of fractional errors.
25+
"""
26+
self.error_arr = error_arr
27+
self.accuracy_result : Union[AccuracyResult, None] = None
28+
29+
def calculateCDF(self) -> AccuracyResult:
30+
"""
31+
Calculate the cumulative distribution function (CDF) of the errors.
32+
33+
Returns:
34+
AccuracyResult: Named tuple containing accuracy, mean absolute error, and CDF DataFrame.
35+
"""
36+
# Calculate accuracy as the percentage of errors within a threshold
37+
accuracy = np.mean(np.abs(self.error_arr) < 0.1)
38+
# Calculate mean absolute error
39+
mean_absolute_error = np.mean(np.abs(self.error_arr))
40+
# Create a DataFrame for CDF
41+
cdf_df = pd.DataFrame({COL_ERROR: np.sort(self.error_arr)})
42+
cdf_df[COL_CDF] = np.arange(1, len(cdf_df) + 1) / len(cdf_df)
43+
#
44+
self.accuracy_result = cast(AccuracyResult, self.accuracy_result)
45+
self.accuracy_result = AccuracyResult(accuracy=accuracy,
46+
mean_absolute_error=mean_absolute_error, cdf_df=cdf_df)
47+
return self.accuracy_result
48+
49+
@classmethod
50+
def getStatistics(cls, error_arr: np.ndarray,
51+
percentiles: List[float]=PERCENTILE_STATISTICS) -> List[float]:
52+
"""
53+
Get accuracy statistics for the given error array.
54+
55+
Args:
56+
error_arr (np.ndarray): Array of fractional errors.
57+
percentiles (List[float]): List of percentiles to calculate.
58+
59+
Returns:
60+
List[float]: List of accuracy statistics for the specified percentiles.
61+
"""
62+
ser = pd.Series(error_arr)
63+
results = ser.quantile(percentiles).values.tolist() # type: ignore
64+
return results
65+
66+
def plotCDF(self, ax: Optional[Axes] = None,
67+
is_plot: bool=True) -> None:
68+
"""
69+
Plot the CDF of the errors.
70+
71+
Args:
72+
ax (plt.Axes, optional): Matplotlib axes object. If None, creates a new figure.
73+
is_plot (bool, optional): Whether to plot the CDF. Defaults to True.
74+
"""
75+
if ax is None:
76+
plt.figure(figsize=(10, 6))
77+
ax = plt.gca()
78+
# Create data
79+
cdf_df = self.calculateCDF().cdf_df
80+
# Plot
81+
ax.plot(cdf_df[COL_ERROR], cdf_df[COL_CDF], marker='o')
82+
ax.set_title('Cumulative Distribution Function (CDF) of Errors')
83+
ax.set_xlabel('Error')
84+
ax.set_ylabel('CDF')
85+
ax.grid()
86+
#
87+
if is_plot:
88+
plt.show()
89+
90+
def plotCDFComparison(self, calculator: 'AccuracyCalculator',
91+
names: Optional[List[str]] = None,
92+
ax: Optional[Axes] = None, is_plot: bool=True) -> None:
93+
"""
94+
Plot the CDF of the errors from this calculator and another calculator.
95+
96+
Args:
97+
calculator (AccuracyCalculator): Another AccuracyCalculator instance.
98+
names (Optional[List[str]]): List of names for the plots.
99+
ax (plt.Axes, optional): Matplotlib axes object. If None, creates a new figure.
100+
is_plot (bool, optional): Whether to plot the CDF. Defaults to True.
101+
"""
102+
if ax is None:
103+
plt.figure(figsize=(10, 6))
104+
ax = plt.gca()
105+
if names is None:
106+
names = ['Current', 'Other']
107+
# Create data
108+
cdf_df = self.calculateCDF().cdf_df
109+
other_cdf_df = calculator.calculateCDF().cdf_df
110+
# Plot
111+
ax.plot(cdf_df[COL_ERROR], cdf_df[COL_CDF], marker='o',
112+
label=names[0])
113+
ax.plot(other_cdf_df[COL_ERROR], other_cdf_df[COL_CDF], marker='x',
114+
label=names[1])
115+
ax.set_title('Cumulative Distribution Function (CDF) Comparison')
116+
ax.set_xlabel('Error')
117+
ax.set_ylabel('CDF')
118+
ax.legend()
119+
if is_plot:
120+
plt.show()

tests/test_accuracy_calculator.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from iplane.accuracy_calculator import AccuracyCalculator, AccuracyResult, PERCENTILE_STATISTICS # type: ignore
2+
3+
import numpy as np # type: ignore
4+
import pandas as pd # type: ignore
5+
import unittest
6+
7+
IGNORE_TESTS = False
8+
IS_PLOT = False
9+
ERROR_ARR = np.random.rand(1000) # Simulated fractional errors
10+
11+
12+
class TestAccuracyCalculator(unittest.TestCase):
13+
14+
def setUp(self):
15+
self.calculator = AccuracyCalculator(ERROR_ARR)
16+
17+
def testConstructor(self):
18+
if IGNORE_TESTS:
19+
return
20+
self.assertIsInstance(self.calculator, AccuracyCalculator)
21+
self.assertIsInstance(self.calculator.error_arr, np.ndarray)
22+
23+
def testCalculateCDF(self):
24+
if IGNORE_TESTS:
25+
return
26+
result = self.calculator.calculateCDF()
27+
self.assertIsInstance(result, AccuracyResult)
28+
self.assertIsInstance(result.cdf_df, pd.DataFrame)
29+
30+
def testGetStatistics(self):
31+
if IGNORE_TESTS:
32+
return
33+
result = AccuracyCalculator.getStatistics(ERROR_ARR)
34+
self.assertIsInstance(result, list)
35+
self.assertEqual(len(result), len(PERCENTILE_STATISTICS))
36+
for idx in range(len(result)):
37+
self.assertIsInstance(result[idx], float)
38+
self.assertGreaterEqual(result[idx], 0.0)
39+
self.assertLessEqual(result[idx], 1.0)
40+
if idx > 0:
41+
self.assertGreaterEqual(result[idx], result[idx - 1])
42+
43+
def testPlotCDF(self):
44+
if IGNORE_TESTS or not IS_PLOT:
45+
return
46+
self.calculator.plotCDF(is_plot=IS_PLOT)
47+
# Check if the plot was created without errors
48+
self.assertTrue(True)
49+
50+
def testPlotCDFComparison(self):
51+
if IGNORE_TESTS:
52+
return
53+
error_arr = np.random.normal(0, 1, 1000) # Simulated fractional errors
54+
other_calculator = AccuracyCalculator(error_arr)
55+
self.calculator.plotCDFComparison(other_calculator,
56+
names=['uniform', 'normal'], is_plot=IS_PLOT)
57+
58+
59+
if __name__ == '__main__':
60+
unittest.main()

0 commit comments

Comments
 (0)