ssscoring.calc
Functions and logic for analyzing and manipulating FlySight dataframes.
1# See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt 2 3""" 4Functions and logic for analyzing and manipulating FlySight dataframes. 5""" 6 7 8from io import BytesIO 9from io import StringIO 10from pathlib import Path 11 12from haversine import haversine 13from haversine import Unit 14 15from ssscoring.constants import BREAKOFF_ALTITUDE 16from ssscoring.constants import DEG_IN_RADIANS 17from ssscoring.constants import EXIT_SPEED 18from ssscoring.constants import FLYSIGHT_FILE_ENCODING 19from ssscoring.constants import FT_IN_M 20from ssscoring.constants import KMH_AS_MS 21from ssscoring.constants import LAST_TIME_TRANCHE 22from ssscoring.constants import MAX_ALTITUDE_METERS 23from ssscoring.constants import MAX_VALID_ELEVATION 24from ssscoring.constants import MPS_2_KMH 25from ssscoring.constants import PERFORMANCE_WINDOW_LENGTH 26from ssscoring.constants import SCORING_INTERVAL 27from ssscoring.constants import SPEED_ACCURACY_THRESHOLD 28from ssscoring.constants import TABLE_INTERVAL 29from ssscoring.constants import VALIDATION_WINDOW_LENGTH 30from ssscoring.datatypes import JumpResults 31from ssscoring.datatypes import JumpStatus 32from ssscoring.datatypes import PerformanceWindow 33from ssscoring.errors import SSScoringError 34from ssscoring.flysight import FlySightVersion 35from ssscoring.flysight import detectFlySightFileVersionOf 36 37import math 38import warnings 39 40import numpy as np 41import pandas as pd 42 43 44# +++ functions +++ 45 46def isValidMinimumAltitude(altitude: float) -> bool: 47 """ 48 Reports whether an `altitude` is below the IPC and USPA valid parameters, 49 or within `BREAKOFF_ALTITUDE` and `PERFORMACE_WINDOW_LENGTH`. In invalid 50 altitude doesn't invalidate a FlySight data file. This function can be used 51 for generating warnings. The stock FlySightViewer scores a speed jump even 52 if the exit was below the minimum altitude. 53 54 See: FAI Competition Rules Speed Skydiving section 5.3 for details. 55 56 Arguments 57 --------- 58 altitude 59 An altitude in meters, calculated as data.hMSL - DZ altitude. 60 61 Returns 62 ------- 63 `True` if the altitude is valid. 64 """ 65 if not isinstance(altitude, float): 66 altitude = float(altitude) 67 minAltitude = BREAKOFF_ALTITUDE+PERFORMANCE_WINDOW_LENGTH 68 return altitude >= minAltitude 69 70 71def isValidMaximumAltitude(altitude: float) -> bool: 72 """ 73 Reports whether an `altitude` is above the maximum altitude allowed by the 74 rules. 75 76 See: FAI Competition Rules Speed Skydiving section 5.3 for details. 77 78 Arguments 79 --------- 80 altitude 81 An altitude in meters, calculated as data.hMSL - DZ altitude. 82 83 Returns 84 ------- 85 `True` if the altitude is valid. 86 87 See 88 --- 89 `ssscoring.constants.MAX_ALTITUDE_FT` 90 `ssscoring.constants.MAX_ALTITUDE_METERS` 91 """ 92 if not isinstance(altitude, float): 93 altitude = float(altitude) 94 return altitude <= MAX_ALTITUDE_METERS 95 96 97def isValidJumpISC(data: pd.DataFrame, 98 window: PerformanceWindow) -> bool: 99 """ 100 **DEPRECATED** - Validates the jump according to ISC/FAI/USPA competition rules. A jump is 101 valid when the speed accuracy parameter is less than 3 m/s for the whole 102 validation window duration. 103 104 Arguments 105 --------- 106 data : pd.DataFramce 107 Jumnp data in SSScoring format 108 window : ssscoring.PerformanceWindow 109 Performance window start, end values in named tuple format 110 111 Returns 112 ------- 113 `True` if the jump is valid according to ISC/FAI/USPA rules. 114 """ 115 warnings.warn('This function is DEPRECATED as of version 2.4.0', UserWarning) 116 if len(data) > 0: 117 accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max() 118 return accuracy < SPEED_ACCURACY_THRESHOLD 119 else: 120 return False 121 122 123def validateJumpISC(data: pd.DataFrame, 124 window: PerformanceWindow) -> JumpStatus: 125 """ 126 Validates the jump according to ISC/FAI/USPA competition rules. A jump is 127 valid when the speed accuracy parameter is less than 3 m/s for the whole 128 validation window duration. 129 130 Arguments 131 --------- 132 data : pd.DataFramce 133 Jumnp data in SSScoring format 134 window : ssscoring.PerformanceWindow 135 Performance window start, end values in named tuple format 136 137 Returns 138 ------- 139 - `JumpStatus.OK` if `data` reflects a valid jump according to ISC rules, 140 where all speed accuracy values < 'SPEED_ACCURACY_THRESHOLD'. 141 - `JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT' if `data` has one or more values 142 within the validation window with a value >= 'SPEED_ACCURACY_THRESHOLD'. 143 144 Raises 145 ------ 146 `SSScoringError' if `data` has a length of zero or it's not initialized. 147 """ 148 if len(data) > 0: 149 accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max() 150 return JumpStatus.OK if accuracy < SPEED_ACCURACY_THRESHOLD else JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT 151 else: 152 raise SSScoringError('data length of zero or invalid') 153 154 155def calculateDistance(start: tuple, end: tuple) -> float: 156 """ 157 Calculate the distance between two terrestrial coordinates points. 158 159 Arguments 160 --------- 161 start 162 A latitude, longitude tuple of floating point numbers. 163 164 end 165 A latitude, longitude tuple of floating point numbers. 166 167 Returns 168 ------- 169 The distance, in meters, between both points. 170 """ 171 return haversine(start, end, unit = Unit.METERS) 172 173 174def convertFlySight2SSScoring(rawData: pd.DataFrame, 175 altitudeDZMeters = 0.0, 176 altitudeDZFt = 0.0): 177 """ 178 Converts a raw dataframe initialized from a FlySight CSV file into the 179 SSScoring file format. The SSScoring format uses more descriptive column 180 headers, adds the altitude in feet, and uses UNIX time instead of an ISO 181 string. 182 183 If both `altitudeDZMeters` and `altitudeDZFt` are zero then hMSL is used. 184 Otherwise, this function adjusts the effective altitude with the value. If 185 both meters and feet values are set this throws an error. 186 187 Arguments 188 --------- 189 rawData : pd.DataFrame 190 FlySight CSV input as a dataframe 191 192 altitudeDZMeters : float 193 Drop zone height above MSL 194 195 altitudeDZFt 196 Drop zone altitudde above MSL 197 198 Returns 199 ------- 200 A dataframe in SSScoring format, featuring these columns: 201 202 - timeUnix 203 - altitudeMSL 204 - altitudeAGL 205 - altitudeMSLFt 206 - altitudeAGLFt 207 - hMetersPerSecond 208 - hKMh (km/h) 209 - vMetersPerSecond 210 - vKMh (km/h) 211 - angle 212 - speedAccuracy (ignore; see ISC documentation) 213 - hMetersPerSecond 214 - hKMh 215 - latitude 216 - longitude 217 - verticalAccuracy 218 - speedAccuracyISC 219 220 Errors 221 ------ 222 `SSScoringError` if the DZ altitude is set in both meters and feet. 223 """ 224 if not isinstance(rawData, pd.DataFrame): 225 raise SSScoringError('convertFlySight2SSScoring input must be a FlySight CSV dataframe') 226 227 if altitudeDZMeters and altitudeDZFt: 228 raise SSScoringError('Cannot set altitude in meters and feet; pick one') 229 230 if altitudeDZMeters: 231 altitudeDZFt = FT_IN_M*altitudeDZMeters 232 if altitudeDZFt: 233 altitudeDZMeters = altitudeDZFt/FT_IN_M 234 235 data = rawData.copy() 236 237 data['altitudeMSLFt'] = data['hMSL'].apply(lambda h: FT_IN_M*h) 238 data['altitudeAGL'] = data.hMSL-altitudeDZMeters 239 data['altitudeAGLFt'] = data.altitudeMSLFt-altitudeDZFt 240 data['timeUnix'] = np.round(data['time'].apply(lambda t: pd.Timestamp(t).timestamp()), decimals = 2) 241 data['hMetersPerSecond'] = (data.velE**2.0+data.velN**2.0)**0.5 242 speedAngle = data['hMetersPerSecond']/data['velD'] 243 speedAngle = np.round(90.0-speedAngle.apply(math.atan)/DEG_IN_RADIANS, decimals = 2) 244 speedAccuracyISC = np.round(data.vAcc.apply(lambda a: (2.0**0.5)*a/3.0), decimals = 2) 245 246 data = pd.DataFrame(data = { 247 'timeUnix': data.timeUnix, 248 'altitudeMSL': data.hMSL, 249 'altitudeAGL': data.altitudeAGL, 250 'altitudeMSLFt': data.altitudeMSLFt, 251 'altitudeAGLFt': data.altitudeAGLFt, 252 'vMetersPerSecond': data.velD, 253 'vKMh': 3.6*data.velD, 254 'speedAngle': speedAngle, 255 'speedAccuracy': data.sAcc, 256 'hMetersPerSecond': data.hMetersPerSecond, 257 'hKMh': 3.6*data.hMetersPerSecond, 258 'latitude': data.lat, 259 'longitude': data.lon, 260 'verticalAccuracy': data.vAcc, 261 'speedAccuracyISC': speedAccuracyISC, 262 }) 263 264 return data 265 266 267def _dataGroups(data): 268 data_ = data.copy() 269 data_['positive'] = (data_.vMetersPerSecond > 0) 270 data_['group'] = (data_.positive != data_.positive.shift(1)).astype(int).cumsum()-1 271 272 return data_ 273 274 275def getSpeedSkydiveFrom(data: pd.DataFrame) -> tuple: 276 """ 277 Take the skydive dataframe and get the speed skydiving data: 278 279 - Exit 280 - Speed skydiving window 281 - Drops data before exit and below breakoff altitude 282 283 Arguments 284 --------- 285 data : pd.DataFrame 286 Jump data in SSScoring format 287 288 Returns 289 ------- 290 A tuple of two elements: 291 292 - A named tuple with performance and validation window data 293 - A dataframe featuring only speed skydiving data 294 295 Warm up FlySight files and non-speed skydiving files may return invalid 296 values: 297 298 - `None` for the `PerformanceWindow` instance 299 - `data`, most likely empty 300 """ 301 if len(data): 302 data = _dataGroups(data) 303 groups = data.group.max()+1 304 305 freeFallGroup = -1 306 MIN_DATA_POINTS = 100 # heuristic 307 MIN_MAX_SPEED = 200 # km/h, heuristic; slower ::= no free fall 308 for group in range(groups): 309 subset = data[data.group == group] 310 if len(subset) >= MIN_DATA_POINTS and subset.vKMh.max() >= MIN_MAX_SPEED: 311 freeFallGroup = group 312 313 data = data[data.group == freeFallGroup] 314 data = data.drop('group', axis = 1).drop('positive', axis = 1) 315 316 data = data[data.altitudeAGL <= MAX_VALID_ELEVATION] 317 if len(data) > 0: 318 exitTime = data[data.vMetersPerSecond > EXIT_SPEED].head(1).timeUnix.iat[0] 319 data = data[data.timeUnix >= exitTime] 320 data = data[data.altitudeAGL >= BREAKOFF_ALTITUDE] 321 322 if len(data): 323 windowStart = data.iloc[0].altitudeAGL 324 windowEnd = windowStart-PERFORMANCE_WINDOW_LENGTH 325 if windowEnd < BREAKOFF_ALTITUDE: 326 windowEnd = BREAKOFF_ALTITUDE 327 328 validationWindowStart = windowEnd+VALIDATION_WINDOW_LENGTH 329 data = data[data.altitudeAGL >= windowEnd] 330 performanceWindow = PerformanceWindow(windowStart, windowEnd, validationWindowStart) 331 else: 332 performanceWindow = None 333 334 return performanceWindow, data 335 else: 336 return None, data 337 338 339def _verticalAcceleration(vKMh: pd.Series, time: pd.Series, interval=TABLE_INTERVAL) -> pd.Series: 340 vAcc = ((vKMh/KMH_AS_MS).diff()/time.diff()).fillna(vKMh/KMH_AS_MS/interval) 341 return vAcc 342 343 344def jumpAnalysisTable(data: pd.DataFrame) -> pd.DataFrame: 345 """ 346 Generates the HCD jump analysis table, with speed data at 5-second intervals 347 after exit. 348 349 Arguments 350 --------- 351 data : pd.DataFrame 352 Jump data in SSScoring format 353 354 Returns 355 ------- 356 A tuple with a pd.DataFrame and the max speed recorded for the jump: 357 358 - A table dataframe with time and speed 359 - a floating point number 360 """ 361 table = None 362 distanceStart = (data.iloc[0].latitude, data.iloc[0].longitude) 363 for column in pd.Series([ 5.0, 10.0, 15.0, 20.0, 25.0, ]): 364 for interval in range(int(column)*10, 10*(int(column)+1)): 365 # Use the next 0.1 sec interval if the current interval tranche has 366 # NaN values. 367 columnRef = interval/10.0 368 timeOffset = data.iloc[0].timeUnix+columnRef 369 tranche = data.query('timeUnix == %f' % timeOffset).copy() 370 tranche['time'] = [ column, ] 371 currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude) 372 tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ] 373 if not tranche.isnull().any().any(): 374 break 375 376 if pd.isna(tranche.iloc[-1].vKMh): 377 tranche = data.tail(1).copy() 378 currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude) 379 tranche['time'] = tranche.timeUnix-data.iloc[0].timeUnix 380 tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ] 381 382 if table is not None: 383 table = pd.concat([ table, tranche, ]) 384 else: 385 table = tranche 386 table = pd.DataFrame({ 387 'time': table.time, 388 'vKMh': table.vKMh, 389 'deltaV': table.vKMh.diff().fillna(table.vKMh), 390 'vAccel m/s²': _verticalAcceleration(table.vKMh, table.time), 391 'speedAngle': table.speedAngle, 392 'angularVel º/s': (table.speedAngle.diff()/table.time.diff()).fillna(table.speedAngle/TABLE_INTERVAL), 393 'deltaAngle': table.speedAngle.diff().fillna(table.speedAngle), 394 'hKMh': table.hKMh, 395 'distanceFromExit (m)': table.distanceFromExit, 396 'altitude (ft)': table.altitudeAGLFt, 397 }) 398 return table 399 400 401def dropNonSkydiveDataFrom(data: pd.DataFrame) -> pd.DataFrame: 402 """ 403 Discards all data rows before maximum altitude, and all "negative" altitude 404 rows because we don't skydive underground (FlySight bug?). 405 406 This is a more accurate mean velocity calculation from a physics point of 407 view, but it differs from the ISC definition using in scoring - which, if we 408 get technical, is the one that counts. 409 410 Arguments 411 --------- 412 data : pd.DataFrame 413 Jump data in SSScoring format (headers differ from FlySight format) 414 415 Returns 416 ------- 417 The jump data for the skydive 418 """ 419 timeMaxAlt = data[data.altitudeAGL == data.altitudeAGL.max()].timeUnix.iloc[0] 420 data = data[data.timeUnix > timeMaxAlt] 421 422 data = data[data.altitudeAGL > 0] 423 424 return data 425 426 427def calcScoreMeanVelocity(data: pd.DataFrame) -> tuple: 428 """ 429 Calculates the speeds over a 3-second interval as the mean of all the speeds 430 recorded within that 3-second window and resolves the maximum speed. 431 432 Arguments 433 --------- 434 data 435 A `pd.dataframe` with speed run data. 436 437 Returns 438 ------- 439 A `tuple` with the best score throughout the speed run, and a dicitionary 440 of the meanVSpeed:spotInTime used in determining the exact scoring speed 441 at every datat point during the speed run. 442 443 Notes 444 ----- 445 This implementation uses iteration instead of binning/factorization because 446 some implementers may be unfamiliar with data manipulation in dataframes 447 and this is a critical function that may be under heavy review. Future 448 versions may revert to dataframe and series/np.array factorization. 449 """ 450 scores = dict() 451 for spot in data.plotTime[::1]: 452 subset = data[(data.plotTime <= spot) & (data.plotTime >= (spot-SCORING_INTERVAL))] 453 scores[np.round(subset.vKMh.mean(), decimals = 2)] = spot 454 return (max(scores), scores) 455 456 457def calcScoreISC(data: pd.DataFrame) -> tuple: 458 """ 459 Calculates the speeds over a 3-second interval as the ds/dt and dt is the 460 is a 3-second sliding interval from exit. The window slider moves along the 461 `plotTime` axis in the dataframe. 462 463 Arguments 464 --------- 465 data 466 A `pd.dataframe` with speed run data. 467 468 Returns 469 ------- 470 A `tuple` with the best score throughout the speed run, and a dicitionary 471 of the meanVSpeed:spotInTime used in determining the exact scoring speed 472 at every datat point during the speed run. 473 """ 474 scores = dict() 475 step = data.plotTime.diff().dropna().mode().iloc[0] 476 end = data.plotTime[-1:].iloc[0]-SCORING_INTERVAL 477 for spot in np.arange(0.0, end, step): 478 intervalStart = np.round(spot, decimals = 2) 479 intervalEnd = np.round(intervalStart+SCORING_INTERVAL, decimals = 2) 480 try: 481 h1 = data[data.plotTime == intervalStart].altitudeAGL.iloc[0] 482 h2 = data[data.plotTime == intervalEnd].altitudeAGL.iloc[0] 483 except IndexError: 484 # TODO: Decide whether to log the missing FlySight samples. 485 continue 486 intervalScore = np.round(MPS_2_KMH*abs(h1-h2)/SCORING_INTERVAL, decimals = 2) 487 scores[intervalScore] = intervalStart 488 return (max(scores), scores) 489 490 491def processJump(data: pd.DataFrame) -> JumpResults: 492 """ 493 Take a dataframe in SSScoring format and process it for display. It 494 serializes all the steps that would be taken from the ssscoring module, but 495 includes some text/HTML data in the output. 496 497 Arguments 498 --------- 499 data: pd.DataFrame 500 A dataframe in SSScoring format 501 502 Returns 503 ------- 504 A `JumpResults` named tuple with these items: 505 506 - `score` speed score 507 - `maxSpeed` maximum speed during the jump 508 - `scores` a Series of every 3-second window scores from exit to breakoff 509 - `data` an updated SSScoring dataframe `plotTime`, where 0 = exit, used 510 for plotting 511 - `window` a named tuple with the exit, breakoff, and validation window 512 altitudes 513 - `table` a dataframe featuring the speeds and altitudes at 5-sec intervals 514 - `color` a string that defines the color for the jump result; possible 515 values are _green_ for valid jump, _red_ for invalid jump, per ISC rules 516 - `result` a string with the legend of _valid_ or _invalid_ jump 517 """ 518 workData = data.copy() 519 workData = dropNonSkydiveDataFrom(workData) 520 window, workData = getSpeedSkydiveFrom(workData) 521 if workData.empty and not window: 522 workData = None 523 maxSpeed = -1.0 524 score = -1.0 525 scores = None 526 table = None 527 window = None 528 jumpStatus = JumpStatus.WARM_UP_FILE 529 else: 530 jumpStatus = validateJumpISC(workData, window) 531 score = None 532 scores = None 533 table = None 534 baseTime = workData.iloc[0].timeUnix 535 workData['plotTime'] = round(workData.timeUnix-baseTime, 2) 536 if jumpStatus == JumpStatus.OK: 537 table = None 538 table = jumpAnalysisTable(workData) 539 maxSpeed = data.vKMh.max() 540 score, scores = calcScoreISC(workData) 541 else: 542 maxSpeed = -1 543 if not len(workData): 544 jumpStatus = JumpStatus.INVALID_SPEED_FILE 545 return JumpResults(workData, maxSpeed, score, scores, table, window, jumpStatus) 546 547 548def _readVersion1CSV(fileThing: str) -> pd.DataFrame: 549 return pd.read_csv(fileThing, skiprows = (1, 1), index_col = False) 550 551 552def _tagVersion1From(fileThing: str) -> str: 553 return fileThing.replace('.CSV', '').replace('.csv', '').replace('/data', '').replace('/', ' ').strip()+':v1' 554 555 556def _tagVersion2From(fileThing: str) -> str: 557 if '/' in fileThing: 558 return fileThing.split('/')[-2]+':v2' 559 else: 560 return fileThing.replace('.CSV', '').replace('.csv', '')+':v2' 561 562 563 564def _readVersion2CSV(jumpFile: str) -> pd.DataFrame: 565 from ssscoring.constants import FLYSIGHT_2_HEADER 566 from ssscoring.flysight import skipOverFS2MetadataRowsIn 567 568 rawData = pd.read_csv(jumpFile, names = FLYSIGHT_2_HEADER, skiprows = 6, index_col = False) 569 rawData = skipOverFS2MetadataRowsIn(rawData) 570 rawData.drop('GNSS', inplace = True, axis = 1) 571 return rawData 572 573 574def getFlySightDataFromCSVBuffer(buffer:bytes, bufferName:str) -> tuple: 575 """ 576 Ingress a buffer with known FlySight or SkyTrax file data for SSScoring 577 processing. 578 579 Arguments 580 --------- 581 buffer 582 A binary data buffer, bag of bytes, containing a known FlySight track file. 583 584 bufferName 585 An arbitrary name for the buffer of type `str`. It's used for constructing 586 the full buffer tag value for human identification. 587 588 Returns 589 ------- 590 A `tuple` with two items: 591 - `rawData` - a dataframe representation of the CSV with the original 592 headers but without the data type header 593 - `tag` - a string with an identifying tag derived from the path name 594 and file version in the form `some name:vX`. It uses the current 595 path as metadata to infer the name. There's no semantics enforcement. 596 597 Raises 598 ------ 599 `SSScoringError` if the CSV file is invalid in any way. 600 """ 601 if not isinstance(buffer, bytes): 602 raise SSScoringError('buffer must be an instance of bytes, a bytes buffer') 603 try: 604 stringIO = StringIO(buffer.decode(FLYSIGHT_FILE_ENCODING)) 605 except Exception as e: 606 raise SSScoringError('invalid buffer endcoding - %s' % str(e)) 607 version = detectFlySightFileVersionOf(buffer) 608 if version == FlySightVersion.V1: 609 rawData = _readVersion1CSV(stringIO) 610 tag = _tagVersion1From(bufferName) 611 elif version == FlySightVersion.V2: 612 rawData = _readVersion2CSV(stringIO) 613 tag = _tagVersion2From(bufferName) 614 return (rawData, tag) 615 616 617def getFlySightDataFromCSVFileName(jumpFile) -> tuple: 618 """ 619 Ingress a known FlySight or SkyTrax file into memory for SSScoring 620 processing. 621 622 Arguments 623 --------- 624 jumpFile 625 A string or `pathlib.Path` object; can be a relative or an asbolute path. 626 627 Returns 628 ------- 629 A `tuple` with two items: 630 - `rawData` - a dataframe representation of the CSV with the original 631 headers but without the data type header 632 - `tag` - a string with an identifying tag derived from the path name 633 and file version in the form `some name:vX`. It uses the current 634 path as metadata to infer the name. There's no semantics enforcement. 635 636 Raises 637 ------ 638 `SSScoringError` if the CSV file is invalid in any way. 639 """ 640 from ssscoring.flysight import validFlySightHeaderIn 641 642 if isinstance(jumpFile, Path): 643 jumpFile = jumpFile.as_posix() 644 elif isinstance(jumpFile, str): 645 pass 646 else: 647 raise SSScoringError('jumpFile must be a string or a Path object') 648 if not validFlySightHeaderIn(jumpFile): 649 raise SSScoringError('%s is an invalid speed skydiving file') 650 version = detectFlySightFileVersionOf(jumpFile) 651 if version == FlySightVersion.V1: 652 rawData = _readVersion1CSV(jumpFile) 653 tag = _tagVersion1From(jumpFile) 654 elif version == FlySightVersion.V2: 655 rawData = _readVersion2CSV(jumpFile) 656 tag = _tagVersion2From(jumpFile) 657 return (rawData, tag) 658 659 660def processAllJumpFiles(jumpFiles: list, altitudeDZMeters = 0.0) -> dict: 661 """ 662 Process all jump files in a list of valid FlySight files. Returns a 663 dictionary of jump results with a human-readable version of the file name. 664 The `jumpFiles` list can be generated by hand or the output of the 665 `ssscoring.fs1.getAllSpeedJumpFilesFrom` called to operate on a data lake. 666 667 Arguments 668 --------- 669 jumpFiles 670 A list of file things that could represent one of these: 671 - file things relative or absolute path names to individual FlySight CSV 672 files. 673 - A specialization of BytesIO, such as the bags of bytes that Streamlit.io 674 generates after uploading and reading a file into the Streamlit 675 environment 676 677 altitudeDZMeters : float 678 Drop zone height above MSL 679 680 Returns 681 ------- 682 dict 683 A dictionary of jump results. The key is a human-readable version of a 684 `jumpFile` name with the extension, path, and extraneous spaces eliminated 685 or replaced by appropriate characters. File names use Unicode, so accents 686 and non-ANSI characters are allowed in file names. 687 688 Raises 689 ------ 690 `SSScoringError` if the jumpFiles object is empty, or if the individual 691 objects in the list aren't `BytesIO`, file name strings, or `Path` 692 instances. 693 """ 694 jumpResults = dict() 695 if not len(jumpFiles): 696 raise SSScoringError('jumpFiles must have at least one element') 697 if not isinstance(jumpFiles, dict) and not isinstance(jumpFiles, list): 698 raise SSScoringError('dict with jump file names and FS versions or list of byte bags expected') 699 if isinstance(jumpFiles, dict): 700 objectsList = sorted(list(jumpFiles.keys())) 701 elif isinstance(jumpFiles, list): 702 objectsList = jumpFiles 703 obj = objectsList[0] 704 if not isinstance(obj, Path) and not isinstance(obj, str) and not isinstance(obj, BytesIO): 705 raise SSScoringError('jumpFiles must contain file-like things or BytesIO objects') 706 for jumpFile in objectsList: 707 if isinstance(jumpFile, BytesIO): 708 rawData, tag = getFlySightDataFromCSVBuffer(jumpFile.getvalue(), jumpFile.name) 709 else: 710 rawData, tag = getFlySightDataFromCSVFileName(jumpFile) 711 jumpResult = processJump(convertFlySight2SSScoring(rawData, altitudeDZMeters = altitudeDZMeters)) 712 jumpResults[tag] = jumpResult 713 return jumpResults 714 715 716def aggregateResults(jumpResults: dict) -> pd.DataFrame: 717 """ 718 Aggregate all the results in a table fashioned after Marco Hepp's and Nklas 719 Daniel's score tracking data. 720 721 Arguments 722 --------- 723 jumpResults: dict 724 A dictionary of jump results, in which each result corresponds to a FlySight 725 file name. See `ssscoring.processAllJumpFiles` for details. 726 727 Returns 728 ------- 729 A dataframe featuring these columns: 730 731 - Score 732 - Speeds at 5, 10, 15, 20, and 25 second tranches 733 - Final time contemplated in the analysis 734 - Max speed 735 736 The dataframe rows are identified by the human readable jump file name. 737 738 Raises 739 ------ 740 `SSSCoringError` if the `jumpResults` object is empty. 741 """ 742 if not len(jumpResults): 743 raise SSScoringError('jumpResults is empty - impossible to collate angles') 744 speeds = pd.DataFrame() 745 for jumpResultIndex in sorted(list(jumpResults.keys())): 746 jumpResult = jumpResults[jumpResultIndex] 747 if jumpResult.status == JumpStatus.OK: 748 t = jumpResult.table 749 finalTime = t.iloc[-1].time 750 t.iloc[-1].time = LAST_TIME_TRANCHE 751 t = pd.pivot_table(t, columns = t.time) 752 d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object) 753 for column in t.columns: 754 d[column] = t[column].vKMh 755 d['finalTime'] = [ finalTime, ] 756 d['maxSpeed'] = jumpResult.maxSpeed 757 758 if speeds.empty: 759 speeds = d.copy() 760 else: 761 speeds = pd.concat([ speeds, d, ]) 762 speeds = speeds.replace(np.nan, 0.0) 763 return speeds.sort_index() 764 765 766def roundedAggregateResults(aggregate: pd.DataFrame) -> pd.DataFrame: 767 """ 768 Aggregate all the results in a table fashioned after Marco Hepp's and Nklas 769 Daniel's score tracking data. All speed results are rounded at `n > x.5` 770 for any value. 771 772 Arguments 773 --------- 774 aggregate: pd.DataFrame 775 A dataframe output of `ssscoring.fs1.aggregateResults`. 776 777 Returns 778 ------- 779 A dataframe featuring the **rounded values** for these columns: 780 781 - Score 782 - Speeds at 5, 10, 15, 20, and 25 second tranches 783 - Max speed 784 785 The `finalTime` column is ignored. 786 787 The dataframe rows are identified by the human readable jump file name. 788 789 This is a less precise version of the `ssscoring.aggregateResults` 790 dataframe, useful during training to keep rounded results available for 791 review. 792 793 Raises 794 ------ 795 `SSSCoringError` if the `jumpResults` object is empty. 796 """ 797 for column in [col for col in aggregate.columns if 'Time' not in str(col)]: 798 aggregate[column] = aggregate[column].apply(round) 799 800 return aggregate 801 802 803def collateAnglesByTimeFromExit(jumpResults: dict) -> pd.DataFrame: 804 """ 805 Collate all the angles by time from the `jumpResults` into a dataframe that 806 features the jump tag as index, the time tranches and the angles at each 807 time tranche. 808 809 Arguments 810 --------- 811 jumpResults: dict 812 A dictionary of jump results, in which each result corresponds to a FlySight 813 file name. See `ssscoring.processAllJumpFiles` for details. 814 815 Returns 816 ------- 817 A dataframe featuring these columns: 818 819 - Score 820 - Angles at 5, 10, 15, 20, and 25 second tranches 821 - Final time contemplated in the analysis 822 823 Raises 824 ------ 825 `SSSCoringError` if the `jumpResults` object is empty. 826 """ 827 if not len(jumpResults): 828 raise SSScoringError('jumpResults is empty - impossible to collate angles') 829 angles = pd.DataFrame() 830 for jumpResultIndex in sorted(list(jumpResults.keys())): 831 jumpResult = jumpResults[jumpResultIndex] 832 if jumpResult.status == JumpStatus.OK: 833 t = jumpResult.table 834 finalTime = t.iloc[-1].time 835 t.iloc[-1].time = LAST_TIME_TRANCHE 836 t = pd.pivot_table(t, columns = t.time) 837 d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object) 838 for column in t.columns: 839 d[column] = t[column].speedAngle 840 d['finalTime'] = [ finalTime, ] 841 842 if angles.empty: 843 angles = d.copy() 844 else: 845 angles = pd.concat([ angles, d, ]) 846 cols = sorted([ column for column in angles.columns if isinstance(column, float) ]) 847 cols = [ 'score', ]+cols+[ 'finalTime', ] 848 angles = angles[cols] 849 angles = angles.replace(np.nan, 0.0) 850 return angles.sort_index() 851 852 853def totalResultsFrom(aggregate: pd.DataFrame) -> pd.DataFrame: 854 """ 855 Calculates the total and mean speeds for an aggregation of speed jumps. 856 857 Arguments 858 --------- 859 aggregate: pd.DataFrame 860 The aggregate results dataframe resulting from calling `ssscoring.aggregateResults` 861 with valid results. 862 863 Returns 864 ------- 865 A dataframe with one row and two columns: 866 867 - totalSpeed ::= the sum of all speeds in the aggregated results 868 - meanSpeed ::= the mean of all speeds 869 - maxSpeed := the absolute max speed over the speed runs set 870 - meanSpeedSTD := scored speeds standar deviation 871 - maxScore ::= the max score among all the speed scores 872 - maxScoreSTD := the max scores standard deviation 873 874 Raises 875 ------ 876 `AttributeError` if aggregate is an empty dataframe or `None`, or if the 877 `aggregate` dataframe doesn't conform to the output of `ssscoring.aggregateResults`. 878 """ 879 if aggregate is None: 880 raise AttributeError('aggregate dataframe is empty') 881 elif isinstance(aggregate, pd.DataFrame) and not len(aggregate): 882 raise AttributeError('aggregate dataframe is empty') 883 884 totals = pd.DataFrame({ 885 'totalScore': [ round(aggregate.score.sum(), 2), ], 886 'mean': [ round(aggregate.score.mean(), 2), ], 887 'deviation': [ round(aggregate.score.std(), 2), ], 888 'maxScore': [ round(aggregate.score.max(), 2), ], 889 'absMaxSpeed': [ round(aggregate.maxSpeed.max(), 2), ], }, index = [ 'totalSpeed'],) 890 return totals
47def isValidMinimumAltitude(altitude: float) -> bool: 48 """ 49 Reports whether an `altitude` is below the IPC and USPA valid parameters, 50 or within `BREAKOFF_ALTITUDE` and `PERFORMACE_WINDOW_LENGTH`. In invalid 51 altitude doesn't invalidate a FlySight data file. This function can be used 52 for generating warnings. The stock FlySightViewer scores a speed jump even 53 if the exit was below the minimum altitude. 54 55 See: FAI Competition Rules Speed Skydiving section 5.3 for details. 56 57 Arguments 58 --------- 59 altitude 60 An altitude in meters, calculated as data.hMSL - DZ altitude. 61 62 Returns 63 ------- 64 `True` if the altitude is valid. 65 """ 66 if not isinstance(altitude, float): 67 altitude = float(altitude) 68 minAltitude = BREAKOFF_ALTITUDE+PERFORMANCE_WINDOW_LENGTH 69 return altitude >= minAltitude
Reports whether an altitude
is below the IPC and USPA valid parameters,
or within BREAKOFF_ALTITUDE
and PERFORMACE_WINDOW_LENGTH
. In invalid
altitude doesn't invalidate a FlySight data file. This function can be used
for generating warnings. The stock FlySightViewer scores a speed jump even
if the exit was below the minimum altitude.
See: FAI Competition Rules Speed Skydiving section 5.3 for details.
Arguments
altitude
An altitude in meters, calculated as data.hMSL - DZ altitude.
Returns
True
if the altitude is valid.
72def isValidMaximumAltitude(altitude: float) -> bool: 73 """ 74 Reports whether an `altitude` is above the maximum altitude allowed by the 75 rules. 76 77 See: FAI Competition Rules Speed Skydiving section 5.3 for details. 78 79 Arguments 80 --------- 81 altitude 82 An altitude in meters, calculated as data.hMSL - DZ altitude. 83 84 Returns 85 ------- 86 `True` if the altitude is valid. 87 88 See 89 --- 90 `ssscoring.constants.MAX_ALTITUDE_FT` 91 `ssscoring.constants.MAX_ALTITUDE_METERS` 92 """ 93 if not isinstance(altitude, float): 94 altitude = float(altitude) 95 return altitude <= MAX_ALTITUDE_METERS
Reports whether an altitude
is above the maximum altitude allowed by the
rules.
See: FAI Competition Rules Speed Skydiving section 5.3 for details.
Arguments
altitude
An altitude in meters, calculated as data.hMSL - DZ altitude.
Returns
True
if the altitude is valid.
See
ssscoring.constants.MAX_ALTITUDE_FT
ssscoring.constants.MAX_ALTITUDE_METERS
98def isValidJumpISC(data: pd.DataFrame, 99 window: PerformanceWindow) -> bool: 100 """ 101 **DEPRECATED** - Validates the jump according to ISC/FAI/USPA competition rules. A jump is 102 valid when the speed accuracy parameter is less than 3 m/s for the whole 103 validation window duration. 104 105 Arguments 106 --------- 107 data : pd.DataFramce 108 Jumnp data in SSScoring format 109 window : ssscoring.PerformanceWindow 110 Performance window start, end values in named tuple format 111 112 Returns 113 ------- 114 `True` if the jump is valid according to ISC/FAI/USPA rules. 115 """ 116 warnings.warn('This function is DEPRECATED as of version 2.4.0', UserWarning) 117 if len(data) > 0: 118 accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max() 119 return accuracy < SPEED_ACCURACY_THRESHOLD 120 else: 121 return False
DEPRECATED - Validates the jump according to ISC/FAI/USPA competition rules. A jump is valid when the speed accuracy parameter is less than 3 m/s for the whole validation window duration.
Arguments
data : pd.DataFramce
Jumnp data in SSScoring format window : ssscoring.PerformanceWindow Performance window start, end values in named tuple format
Returns
True
if the jump is valid according to ISC/FAI/USPA rules.
124def validateJumpISC(data: pd.DataFrame, 125 window: PerformanceWindow) -> JumpStatus: 126 """ 127 Validates the jump according to ISC/FAI/USPA competition rules. A jump is 128 valid when the speed accuracy parameter is less than 3 m/s for the whole 129 validation window duration. 130 131 Arguments 132 --------- 133 data : pd.DataFramce 134 Jumnp data in SSScoring format 135 window : ssscoring.PerformanceWindow 136 Performance window start, end values in named tuple format 137 138 Returns 139 ------- 140 - `JumpStatus.OK` if `data` reflects a valid jump according to ISC rules, 141 where all speed accuracy values < 'SPEED_ACCURACY_THRESHOLD'. 142 - `JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT' if `data` has one or more values 143 within the validation window with a value >= 'SPEED_ACCURACY_THRESHOLD'. 144 145 Raises 146 ------ 147 `SSScoringError' if `data` has a length of zero or it's not initialized. 148 """ 149 if len(data) > 0: 150 accuracy = data[data.altitudeAGL <= window.validationStart].speedAccuracyISC.max() 151 return JumpStatus.OK if accuracy < SPEED_ACCURACY_THRESHOLD else JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT 152 else: 153 raise SSScoringError('data length of zero or invalid')
Validates the jump according to ISC/FAI/USPA competition rules. A jump is valid when the speed accuracy parameter is less than 3 m/s for the whole validation window duration.
Arguments
data : pd.DataFramce
Jumnp data in SSScoring format window : ssscoring.PerformanceWindow Performance window start, end values in named tuple format
Returns
JumpStatus.OK
ifdata
reflects a valid jump according to ISC rules, where all speed accuracy values < 'SPEED_ACCURACY_THRESHOLD'.JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT' if
data` has one or more values within the validation window with a value >= 'SPEED_ACCURACY_THRESHOLD'.
Raises
SSScoringError' if
data` has a length of zero or it's not initialized.
156def calculateDistance(start: tuple, end: tuple) -> float: 157 """ 158 Calculate the distance between two terrestrial coordinates points. 159 160 Arguments 161 --------- 162 start 163 A latitude, longitude tuple of floating point numbers. 164 165 end 166 A latitude, longitude tuple of floating point numbers. 167 168 Returns 169 ------- 170 The distance, in meters, between both points. 171 """ 172 return haversine(start, end, unit = Unit.METERS)
Calculate the distance between two terrestrial coordinates points.
Arguments
start
A latitude, longitude tuple of floating point numbers.
end
A latitude, longitude tuple of floating point numbers.
Returns
The distance, in meters, between both points.
175def convertFlySight2SSScoring(rawData: pd.DataFrame, 176 altitudeDZMeters = 0.0, 177 altitudeDZFt = 0.0): 178 """ 179 Converts a raw dataframe initialized from a FlySight CSV file into the 180 SSScoring file format. The SSScoring format uses more descriptive column 181 headers, adds the altitude in feet, and uses UNIX time instead of an ISO 182 string. 183 184 If both `altitudeDZMeters` and `altitudeDZFt` are zero then hMSL is used. 185 Otherwise, this function adjusts the effective altitude with the value. If 186 both meters and feet values are set this throws an error. 187 188 Arguments 189 --------- 190 rawData : pd.DataFrame 191 FlySight CSV input as a dataframe 192 193 altitudeDZMeters : float 194 Drop zone height above MSL 195 196 altitudeDZFt 197 Drop zone altitudde above MSL 198 199 Returns 200 ------- 201 A dataframe in SSScoring format, featuring these columns: 202 203 - timeUnix 204 - altitudeMSL 205 - altitudeAGL 206 - altitudeMSLFt 207 - altitudeAGLFt 208 - hMetersPerSecond 209 - hKMh (km/h) 210 - vMetersPerSecond 211 - vKMh (km/h) 212 - angle 213 - speedAccuracy (ignore; see ISC documentation) 214 - hMetersPerSecond 215 - hKMh 216 - latitude 217 - longitude 218 - verticalAccuracy 219 - speedAccuracyISC 220 221 Errors 222 ------ 223 `SSScoringError` if the DZ altitude is set in both meters and feet. 224 """ 225 if not isinstance(rawData, pd.DataFrame): 226 raise SSScoringError('convertFlySight2SSScoring input must be a FlySight CSV dataframe') 227 228 if altitudeDZMeters and altitudeDZFt: 229 raise SSScoringError('Cannot set altitude in meters and feet; pick one') 230 231 if altitudeDZMeters: 232 altitudeDZFt = FT_IN_M*altitudeDZMeters 233 if altitudeDZFt: 234 altitudeDZMeters = altitudeDZFt/FT_IN_M 235 236 data = rawData.copy() 237 238 data['altitudeMSLFt'] = data['hMSL'].apply(lambda h: FT_IN_M*h) 239 data['altitudeAGL'] = data.hMSL-altitudeDZMeters 240 data['altitudeAGLFt'] = data.altitudeMSLFt-altitudeDZFt 241 data['timeUnix'] = np.round(data['time'].apply(lambda t: pd.Timestamp(t).timestamp()), decimals = 2) 242 data['hMetersPerSecond'] = (data.velE**2.0+data.velN**2.0)**0.5 243 speedAngle = data['hMetersPerSecond']/data['velD'] 244 speedAngle = np.round(90.0-speedAngle.apply(math.atan)/DEG_IN_RADIANS, decimals = 2) 245 speedAccuracyISC = np.round(data.vAcc.apply(lambda a: (2.0**0.5)*a/3.0), decimals = 2) 246 247 data = pd.DataFrame(data = { 248 'timeUnix': data.timeUnix, 249 'altitudeMSL': data.hMSL, 250 'altitudeAGL': data.altitudeAGL, 251 'altitudeMSLFt': data.altitudeMSLFt, 252 'altitudeAGLFt': data.altitudeAGLFt, 253 'vMetersPerSecond': data.velD, 254 'vKMh': 3.6*data.velD, 255 'speedAngle': speedAngle, 256 'speedAccuracy': data.sAcc, 257 'hMetersPerSecond': data.hMetersPerSecond, 258 'hKMh': 3.6*data.hMetersPerSecond, 259 'latitude': data.lat, 260 'longitude': data.lon, 261 'verticalAccuracy': data.vAcc, 262 'speedAccuracyISC': speedAccuracyISC, 263 }) 264 265 return data
Converts a raw dataframe initialized from a FlySight CSV file into the SSScoring file format. The SSScoring format uses more descriptive column headers, adds the altitude in feet, and uses UNIX time instead of an ISO string.
If both altitudeDZMeters
and altitudeDZFt
are zero then hMSL is used.
Otherwise, this function adjusts the effective altitude with the value. If
both meters and feet values are set this throws an error.
Arguments
rawData : pd.DataFrame
FlySight CSV input as a dataframe
altitudeDZMeters : float
Drop zone height above MSL
altitudeDZFt
Drop zone altitudde above MSL
Returns
A dataframe in SSScoring format, featuring these columns:
- timeUnix
- altitudeMSL
- altitudeAGL
- altitudeMSLFt
- altitudeAGLFt
- hMetersPerSecond
- hKMh (km/h)
- vMetersPerSecond
- vKMh (km/h)
- angle
- speedAccuracy (ignore; see ISC documentation)
- hMetersPerSecond
- hKMh
- latitude
- longitude
- verticalAccuracy
- speedAccuracyISC
Errors
SSScoringError
if the DZ altitude is set in both meters and feet.
276def getSpeedSkydiveFrom(data: pd.DataFrame) -> tuple: 277 """ 278 Take the skydive dataframe and get the speed skydiving data: 279 280 - Exit 281 - Speed skydiving window 282 - Drops data before exit and below breakoff altitude 283 284 Arguments 285 --------- 286 data : pd.DataFrame 287 Jump data in SSScoring format 288 289 Returns 290 ------- 291 A tuple of two elements: 292 293 - A named tuple with performance and validation window data 294 - A dataframe featuring only speed skydiving data 295 296 Warm up FlySight files and non-speed skydiving files may return invalid 297 values: 298 299 - `None` for the `PerformanceWindow` instance 300 - `data`, most likely empty 301 """ 302 if len(data): 303 data = _dataGroups(data) 304 groups = data.group.max()+1 305 306 freeFallGroup = -1 307 MIN_DATA_POINTS = 100 # heuristic 308 MIN_MAX_SPEED = 200 # km/h, heuristic; slower ::= no free fall 309 for group in range(groups): 310 subset = data[data.group == group] 311 if len(subset) >= MIN_DATA_POINTS and subset.vKMh.max() >= MIN_MAX_SPEED: 312 freeFallGroup = group 313 314 data = data[data.group == freeFallGroup] 315 data = data.drop('group', axis = 1).drop('positive', axis = 1) 316 317 data = data[data.altitudeAGL <= MAX_VALID_ELEVATION] 318 if len(data) > 0: 319 exitTime = data[data.vMetersPerSecond > EXIT_SPEED].head(1).timeUnix.iat[0] 320 data = data[data.timeUnix >= exitTime] 321 data = data[data.altitudeAGL >= BREAKOFF_ALTITUDE] 322 323 if len(data): 324 windowStart = data.iloc[0].altitudeAGL 325 windowEnd = windowStart-PERFORMANCE_WINDOW_LENGTH 326 if windowEnd < BREAKOFF_ALTITUDE: 327 windowEnd = BREAKOFF_ALTITUDE 328 329 validationWindowStart = windowEnd+VALIDATION_WINDOW_LENGTH 330 data = data[data.altitudeAGL >= windowEnd] 331 performanceWindow = PerformanceWindow(windowStart, windowEnd, validationWindowStart) 332 else: 333 performanceWindow = None 334 335 return performanceWindow, data 336 else: 337 return None, data
Take the skydive dataframe and get the speed skydiving data:
- Exit
- Speed skydiving window
- Drops data before exit and below breakoff altitude
Arguments
data : pd.DataFrame
Jump data in SSScoring format
Returns
A tuple of two elements:
- A named tuple with performance and validation window data
- A dataframe featuring only speed skydiving data
Warm up FlySight files and non-speed skydiving files may return invalid values:
None
for thePerformanceWindow
instancedata
, most likely empty
345def jumpAnalysisTable(data: pd.DataFrame) -> pd.DataFrame: 346 """ 347 Generates the HCD jump analysis table, with speed data at 5-second intervals 348 after exit. 349 350 Arguments 351 --------- 352 data : pd.DataFrame 353 Jump data in SSScoring format 354 355 Returns 356 ------- 357 A tuple with a pd.DataFrame and the max speed recorded for the jump: 358 359 - A table dataframe with time and speed 360 - a floating point number 361 """ 362 table = None 363 distanceStart = (data.iloc[0].latitude, data.iloc[0].longitude) 364 for column in pd.Series([ 5.0, 10.0, 15.0, 20.0, 25.0, ]): 365 for interval in range(int(column)*10, 10*(int(column)+1)): 366 # Use the next 0.1 sec interval if the current interval tranche has 367 # NaN values. 368 columnRef = interval/10.0 369 timeOffset = data.iloc[0].timeUnix+columnRef 370 tranche = data.query('timeUnix == %f' % timeOffset).copy() 371 tranche['time'] = [ column, ] 372 currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude) 373 tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ] 374 if not tranche.isnull().any().any(): 375 break 376 377 if pd.isna(tranche.iloc[-1].vKMh): 378 tranche = data.tail(1).copy() 379 currentPosition = (tranche.iloc[0].latitude, tranche.iloc[0].longitude) 380 tranche['time'] = tranche.timeUnix-data.iloc[0].timeUnix 381 tranche['distanceFromExit'] = [ round(calculateDistance(distanceStart, currentPosition), 2), ] 382 383 if table is not None: 384 table = pd.concat([ table, tranche, ]) 385 else: 386 table = tranche 387 table = pd.DataFrame({ 388 'time': table.time, 389 'vKMh': table.vKMh, 390 'deltaV': table.vKMh.diff().fillna(table.vKMh), 391 'vAccel m/s²': _verticalAcceleration(table.vKMh, table.time), 392 'speedAngle': table.speedAngle, 393 'angularVel º/s': (table.speedAngle.diff()/table.time.diff()).fillna(table.speedAngle/TABLE_INTERVAL), 394 'deltaAngle': table.speedAngle.diff().fillna(table.speedAngle), 395 'hKMh': table.hKMh, 396 'distanceFromExit (m)': table.distanceFromExit, 397 'altitude (ft)': table.altitudeAGLFt, 398 }) 399 return table
Generates the HCD jump analysis table, with speed data at 5-second intervals after exit.
Arguments
data : pd.DataFrame
Jump data in SSScoring format
Returns
A tuple with a pd.DataFrame and the max speed recorded for the jump:
- A table dataframe with time and speed
- a floating point number
402def dropNonSkydiveDataFrom(data: pd.DataFrame) -> pd.DataFrame: 403 """ 404 Discards all data rows before maximum altitude, and all "negative" altitude 405 rows because we don't skydive underground (FlySight bug?). 406 407 This is a more accurate mean velocity calculation from a physics point of 408 view, but it differs from the ISC definition using in scoring - which, if we 409 get technical, is the one that counts. 410 411 Arguments 412 --------- 413 data : pd.DataFrame 414 Jump data in SSScoring format (headers differ from FlySight format) 415 416 Returns 417 ------- 418 The jump data for the skydive 419 """ 420 timeMaxAlt = data[data.altitudeAGL == data.altitudeAGL.max()].timeUnix.iloc[0] 421 data = data[data.timeUnix > timeMaxAlt] 422 423 data = data[data.altitudeAGL > 0] 424 425 return data
Discards all data rows before maximum altitude, and all "negative" altitude rows because we don't skydive underground (FlySight bug?).
This is a more accurate mean velocity calculation from a physics point of view, but it differs from the ISC definition using in scoring - which, if we get technical, is the one that counts.
Arguments
data : pd.DataFrame
Jump data in SSScoring format (headers differ from FlySight format)
Returns
The jump data for the skydive
428def calcScoreMeanVelocity(data: pd.DataFrame) -> tuple: 429 """ 430 Calculates the speeds over a 3-second interval as the mean of all the speeds 431 recorded within that 3-second window and resolves the maximum speed. 432 433 Arguments 434 --------- 435 data 436 A `pd.dataframe` with speed run data. 437 438 Returns 439 ------- 440 A `tuple` with the best score throughout the speed run, and a dicitionary 441 of the meanVSpeed:spotInTime used in determining the exact scoring speed 442 at every datat point during the speed run. 443 444 Notes 445 ----- 446 This implementation uses iteration instead of binning/factorization because 447 some implementers may be unfamiliar with data manipulation in dataframes 448 and this is a critical function that may be under heavy review. Future 449 versions may revert to dataframe and series/np.array factorization. 450 """ 451 scores = dict() 452 for spot in data.plotTime[::1]: 453 subset = data[(data.plotTime <= spot) & (data.plotTime >= (spot-SCORING_INTERVAL))] 454 scores[np.round(subset.vKMh.mean(), decimals = 2)] = spot 455 return (max(scores), scores)
Calculates the speeds over a 3-second interval as the mean of all the speeds recorded within that 3-second window and resolves the maximum speed.
Arguments
data
A pd.dataframe
with speed run data.
Returns
A tuple
with the best score throughout the speed run, and a dicitionary
of the meanVSpeed:spotInTime used in determining the exact scoring speed
at every datat point during the speed run.
Notes
This implementation uses iteration instead of binning/factorization because some implementers may be unfamiliar with data manipulation in dataframes and this is a critical function that may be under heavy review. Future versions may revert to dataframe and series/np.array factorization.
458def calcScoreISC(data: pd.DataFrame) -> tuple: 459 """ 460 Calculates the speeds over a 3-second interval as the ds/dt and dt is the 461 is a 3-second sliding interval from exit. The window slider moves along the 462 `plotTime` axis in the dataframe. 463 464 Arguments 465 --------- 466 data 467 A `pd.dataframe` with speed run data. 468 469 Returns 470 ------- 471 A `tuple` with the best score throughout the speed run, and a dicitionary 472 of the meanVSpeed:spotInTime used in determining the exact scoring speed 473 at every datat point during the speed run. 474 """ 475 scores = dict() 476 step = data.plotTime.diff().dropna().mode().iloc[0] 477 end = data.plotTime[-1:].iloc[0]-SCORING_INTERVAL 478 for spot in np.arange(0.0, end, step): 479 intervalStart = np.round(spot, decimals = 2) 480 intervalEnd = np.round(intervalStart+SCORING_INTERVAL, decimals = 2) 481 try: 482 h1 = data[data.plotTime == intervalStart].altitudeAGL.iloc[0] 483 h2 = data[data.plotTime == intervalEnd].altitudeAGL.iloc[0] 484 except IndexError: 485 # TODO: Decide whether to log the missing FlySight samples. 486 continue 487 intervalScore = np.round(MPS_2_KMH*abs(h1-h2)/SCORING_INTERVAL, decimals = 2) 488 scores[intervalScore] = intervalStart 489 return (max(scores), scores)
Calculates the speeds over a 3-second interval as the ds/dt and dt is the
is a 3-second sliding interval from exit. The window slider moves along the
plotTime
axis in the dataframe.
Arguments
data
A pd.dataframe
with speed run data.
Returns
A tuple
with the best score throughout the speed run, and a dicitionary
of the meanVSpeed:spotInTime used in determining the exact scoring speed
at every datat point during the speed run.
492def processJump(data: pd.DataFrame) -> JumpResults: 493 """ 494 Take a dataframe in SSScoring format and process it for display. It 495 serializes all the steps that would be taken from the ssscoring module, but 496 includes some text/HTML data in the output. 497 498 Arguments 499 --------- 500 data: pd.DataFrame 501 A dataframe in SSScoring format 502 503 Returns 504 ------- 505 A `JumpResults` named tuple with these items: 506 507 - `score` speed score 508 - `maxSpeed` maximum speed during the jump 509 - `scores` a Series of every 3-second window scores from exit to breakoff 510 - `data` an updated SSScoring dataframe `plotTime`, where 0 = exit, used 511 for plotting 512 - `window` a named tuple with the exit, breakoff, and validation window 513 altitudes 514 - `table` a dataframe featuring the speeds and altitudes at 5-sec intervals 515 - `color` a string that defines the color for the jump result; possible 516 values are _green_ for valid jump, _red_ for invalid jump, per ISC rules 517 - `result` a string with the legend of _valid_ or _invalid_ jump 518 """ 519 workData = data.copy() 520 workData = dropNonSkydiveDataFrom(workData) 521 window, workData = getSpeedSkydiveFrom(workData) 522 if workData.empty and not window: 523 workData = None 524 maxSpeed = -1.0 525 score = -1.0 526 scores = None 527 table = None 528 window = None 529 jumpStatus = JumpStatus.WARM_UP_FILE 530 else: 531 jumpStatus = validateJumpISC(workData, window) 532 score = None 533 scores = None 534 table = None 535 baseTime = workData.iloc[0].timeUnix 536 workData['plotTime'] = round(workData.timeUnix-baseTime, 2) 537 if jumpStatus == JumpStatus.OK: 538 table = None 539 table = jumpAnalysisTable(workData) 540 maxSpeed = data.vKMh.max() 541 score, scores = calcScoreISC(workData) 542 else: 543 maxSpeed = -1 544 if not len(workData): 545 jumpStatus = JumpStatus.INVALID_SPEED_FILE 546 return JumpResults(workData, maxSpeed, score, scores, table, window, jumpStatus)
Take a dataframe in SSScoring format and process it for display. It serializes all the steps that would be taken from the ssscoring module, but includes some text/HTML data in the output.
Arguments
data: pd.DataFrame
A dataframe in SSScoring format
Returns
A JumpResults
named tuple with these items:
score
speed scoremaxSpeed
maximum speed during the jumpscores
a Series of every 3-second window scores from exit to breakoffdata
an updated SSScoring dataframeplotTime
, where 0 = exit, used for plottingwindow
a named tuple with the exit, breakoff, and validation window altitudestable
a dataframe featuring the speeds and altitudes at 5-sec intervalscolor
a string that defines the color for the jump result; possible values are _green_ for valid jump, _red_ for invalid jump, per ISC rulesresult
a string with the legend of _valid_ or _invalid_ jump
575def getFlySightDataFromCSVBuffer(buffer:bytes, bufferName:str) -> tuple: 576 """ 577 Ingress a buffer with known FlySight or SkyTrax file data for SSScoring 578 processing. 579 580 Arguments 581 --------- 582 buffer 583 A binary data buffer, bag of bytes, containing a known FlySight track file. 584 585 bufferName 586 An arbitrary name for the buffer of type `str`. It's used for constructing 587 the full buffer tag value for human identification. 588 589 Returns 590 ------- 591 A `tuple` with two items: 592 - `rawData` - a dataframe representation of the CSV with the original 593 headers but without the data type header 594 - `tag` - a string with an identifying tag derived from the path name 595 and file version in the form `some name:vX`. It uses the current 596 path as metadata to infer the name. There's no semantics enforcement. 597 598 Raises 599 ------ 600 `SSScoringError` if the CSV file is invalid in any way. 601 """ 602 if not isinstance(buffer, bytes): 603 raise SSScoringError('buffer must be an instance of bytes, a bytes buffer') 604 try: 605 stringIO = StringIO(buffer.decode(FLYSIGHT_FILE_ENCODING)) 606 except Exception as e: 607 raise SSScoringError('invalid buffer endcoding - %s' % str(e)) 608 version = detectFlySightFileVersionOf(buffer) 609 if version == FlySightVersion.V1: 610 rawData = _readVersion1CSV(stringIO) 611 tag = _tagVersion1From(bufferName) 612 elif version == FlySightVersion.V2: 613 rawData = _readVersion2CSV(stringIO) 614 tag = _tagVersion2From(bufferName) 615 return (rawData, tag)
Ingress a buffer with known FlySight or SkyTrax file data for SSScoring processing.
Arguments
buffer
A binary data buffer, bag of bytes, containing a known FlySight track file.
bufferName
An arbitrary name for the buffer of type str
. It's used for constructing
the full buffer tag value for human identification.
Returns
A tuple
with two items:
- rawData
- a dataframe representation of the CSV with the original
headers but without the data type header
- tag
- a string with an identifying tag derived from the path name
and file version in the form some name:vX
. It uses the current
path as metadata to infer the name. There's no semantics enforcement.
Raises
SSScoringError
if the CSV file is invalid in any way.
618def getFlySightDataFromCSVFileName(jumpFile) -> tuple: 619 """ 620 Ingress a known FlySight or SkyTrax file into memory for SSScoring 621 processing. 622 623 Arguments 624 --------- 625 jumpFile 626 A string or `pathlib.Path` object; can be a relative or an asbolute path. 627 628 Returns 629 ------- 630 A `tuple` with two items: 631 - `rawData` - a dataframe representation of the CSV with the original 632 headers but without the data type header 633 - `tag` - a string with an identifying tag derived from the path name 634 and file version in the form `some name:vX`. It uses the current 635 path as metadata to infer the name. There's no semantics enforcement. 636 637 Raises 638 ------ 639 `SSScoringError` if the CSV file is invalid in any way. 640 """ 641 from ssscoring.flysight import validFlySightHeaderIn 642 643 if isinstance(jumpFile, Path): 644 jumpFile = jumpFile.as_posix() 645 elif isinstance(jumpFile, str): 646 pass 647 else: 648 raise SSScoringError('jumpFile must be a string or a Path object') 649 if not validFlySightHeaderIn(jumpFile): 650 raise SSScoringError('%s is an invalid speed skydiving file') 651 version = detectFlySightFileVersionOf(jumpFile) 652 if version == FlySightVersion.V1: 653 rawData = _readVersion1CSV(jumpFile) 654 tag = _tagVersion1From(jumpFile) 655 elif version == FlySightVersion.V2: 656 rawData = _readVersion2CSV(jumpFile) 657 tag = _tagVersion2From(jumpFile) 658 return (rawData, tag)
Ingress a known FlySight or SkyTrax file into memory for SSScoring processing.
Arguments
jumpFile
A string or pathlib.Path
object; can be a relative or an asbolute path.
Returns
A tuple
with two items:
- rawData
- a dataframe representation of the CSV with the original
headers but without the data type header
- tag
- a string with an identifying tag derived from the path name
and file version in the form some name:vX
. It uses the current
path as metadata to infer the name. There's no semantics enforcement.
Raises
SSScoringError
if the CSV file is invalid in any way.
661def processAllJumpFiles(jumpFiles: list, altitudeDZMeters = 0.0) -> dict: 662 """ 663 Process all jump files in a list of valid FlySight files. Returns a 664 dictionary of jump results with a human-readable version of the file name. 665 The `jumpFiles` list can be generated by hand or the output of the 666 `ssscoring.fs1.getAllSpeedJumpFilesFrom` called to operate on a data lake. 667 668 Arguments 669 --------- 670 jumpFiles 671 A list of file things that could represent one of these: 672 - file things relative or absolute path names to individual FlySight CSV 673 files. 674 - A specialization of BytesIO, such as the bags of bytes that Streamlit.io 675 generates after uploading and reading a file into the Streamlit 676 environment 677 678 altitudeDZMeters : float 679 Drop zone height above MSL 680 681 Returns 682 ------- 683 dict 684 A dictionary of jump results. The key is a human-readable version of a 685 `jumpFile` name with the extension, path, and extraneous spaces eliminated 686 or replaced by appropriate characters. File names use Unicode, so accents 687 and non-ANSI characters are allowed in file names. 688 689 Raises 690 ------ 691 `SSScoringError` if the jumpFiles object is empty, or if the individual 692 objects in the list aren't `BytesIO`, file name strings, or `Path` 693 instances. 694 """ 695 jumpResults = dict() 696 if not len(jumpFiles): 697 raise SSScoringError('jumpFiles must have at least one element') 698 if not isinstance(jumpFiles, dict) and not isinstance(jumpFiles, list): 699 raise SSScoringError('dict with jump file names and FS versions or list of byte bags expected') 700 if isinstance(jumpFiles, dict): 701 objectsList = sorted(list(jumpFiles.keys())) 702 elif isinstance(jumpFiles, list): 703 objectsList = jumpFiles 704 obj = objectsList[0] 705 if not isinstance(obj, Path) and not isinstance(obj, str) and not isinstance(obj, BytesIO): 706 raise SSScoringError('jumpFiles must contain file-like things or BytesIO objects') 707 for jumpFile in objectsList: 708 if isinstance(jumpFile, BytesIO): 709 rawData, tag = getFlySightDataFromCSVBuffer(jumpFile.getvalue(), jumpFile.name) 710 else: 711 rawData, tag = getFlySightDataFromCSVFileName(jumpFile) 712 jumpResult = processJump(convertFlySight2SSScoring(rawData, altitudeDZMeters = altitudeDZMeters)) 713 jumpResults[tag] = jumpResult 714 return jumpResults
Process all jump files in a list of valid FlySight files. Returns a
dictionary of jump results with a human-readable version of the file name.
The jumpFiles
list can be generated by hand or the output of the
ssscoring.fs1.getAllSpeedJumpFilesFrom
called to operate on a data lake.
Arguments
jumpFiles
A list of file things that could represent one of these:
- file things relative or absolute path names to individual FlySight CSV files.
- A specialization of BytesIO, such as the bags of bytes that Streamlit.io generates after uploading and reading a file into the Streamlit environment
altitudeDZMeters : float
Drop zone height above MSL
Returns
dict
A dictionary of jump results. The key is a human-readable version of a
jumpFile
name with the extension, path, and extraneous spaces eliminated
or replaced by appropriate characters. File names use Unicode, so accents
and non-ANSI characters are allowed in file names.
Raises
SSScoringError
if the jumpFiles object is empty, or if the individual
objects in the list aren't BytesIO
, file name strings, or Path
instances.
717def aggregateResults(jumpResults: dict) -> pd.DataFrame: 718 """ 719 Aggregate all the results in a table fashioned after Marco Hepp's and Nklas 720 Daniel's score tracking data. 721 722 Arguments 723 --------- 724 jumpResults: dict 725 A dictionary of jump results, in which each result corresponds to a FlySight 726 file name. See `ssscoring.processAllJumpFiles` for details. 727 728 Returns 729 ------- 730 A dataframe featuring these columns: 731 732 - Score 733 - Speeds at 5, 10, 15, 20, and 25 second tranches 734 - Final time contemplated in the analysis 735 - Max speed 736 737 The dataframe rows are identified by the human readable jump file name. 738 739 Raises 740 ------ 741 `SSSCoringError` if the `jumpResults` object is empty. 742 """ 743 if not len(jumpResults): 744 raise SSScoringError('jumpResults is empty - impossible to collate angles') 745 speeds = pd.DataFrame() 746 for jumpResultIndex in sorted(list(jumpResults.keys())): 747 jumpResult = jumpResults[jumpResultIndex] 748 if jumpResult.status == JumpStatus.OK: 749 t = jumpResult.table 750 finalTime = t.iloc[-1].time 751 t.iloc[-1].time = LAST_TIME_TRANCHE 752 t = pd.pivot_table(t, columns = t.time) 753 d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object) 754 for column in t.columns: 755 d[column] = t[column].vKMh 756 d['finalTime'] = [ finalTime, ] 757 d['maxSpeed'] = jumpResult.maxSpeed 758 759 if speeds.empty: 760 speeds = d.copy() 761 else: 762 speeds = pd.concat([ speeds, d, ]) 763 speeds = speeds.replace(np.nan, 0.0) 764 return speeds.sort_index()
Aggregate all the results in a table fashioned after Marco Hepp's and Nklas Daniel's score tracking data.
Arguments
jumpResults: dict
A dictionary of jump results, in which each result corresponds to a FlySight
file name. See ssscoring.processAllJumpFiles
for details.
Returns
A dataframe featuring these columns:
- Score
- Speeds at 5, 10, 15, 20, and 25 second tranches
- Final time contemplated in the analysis
- Max speed
The dataframe rows are identified by the human readable jump file name.
Raises
SSSCoringError
if the jumpResults
object is empty.
767def roundedAggregateResults(aggregate: pd.DataFrame) -> pd.DataFrame: 768 """ 769 Aggregate all the results in a table fashioned after Marco Hepp's and Nklas 770 Daniel's score tracking data. All speed results are rounded at `n > x.5` 771 for any value. 772 773 Arguments 774 --------- 775 aggregate: pd.DataFrame 776 A dataframe output of `ssscoring.fs1.aggregateResults`. 777 778 Returns 779 ------- 780 A dataframe featuring the **rounded values** for these columns: 781 782 - Score 783 - Speeds at 5, 10, 15, 20, and 25 second tranches 784 - Max speed 785 786 The `finalTime` column is ignored. 787 788 The dataframe rows are identified by the human readable jump file name. 789 790 This is a less precise version of the `ssscoring.aggregateResults` 791 dataframe, useful during training to keep rounded results available for 792 review. 793 794 Raises 795 ------ 796 `SSSCoringError` if the `jumpResults` object is empty. 797 """ 798 for column in [col for col in aggregate.columns if 'Time' not in str(col)]: 799 aggregate[column] = aggregate[column].apply(round) 800 801 return aggregate
Aggregate all the results in a table fashioned after Marco Hepp's and Nklas
Daniel's score tracking data. All speed results are rounded at n > x.5
for any value.
Arguments
aggregate: pd.DataFrame
A dataframe output of ssscoring.fs1.aggregateResults
.
Returns
A dataframe featuring the rounded values for these columns:
- Score
- Speeds at 5, 10, 15, 20, and 25 second tranches
- Max speed
The finalTime
column is ignored.
The dataframe rows are identified by the human readable jump file name.
This is a less precise version of the ssscoring.aggregateResults
dataframe, useful during training to keep rounded results available for
review.
Raises
SSSCoringError
if the jumpResults
object is empty.
804def collateAnglesByTimeFromExit(jumpResults: dict) -> pd.DataFrame: 805 """ 806 Collate all the angles by time from the `jumpResults` into a dataframe that 807 features the jump tag as index, the time tranches and the angles at each 808 time tranche. 809 810 Arguments 811 --------- 812 jumpResults: dict 813 A dictionary of jump results, in which each result corresponds to a FlySight 814 file name. See `ssscoring.processAllJumpFiles` for details. 815 816 Returns 817 ------- 818 A dataframe featuring these columns: 819 820 - Score 821 - Angles at 5, 10, 15, 20, and 25 second tranches 822 - Final time contemplated in the analysis 823 824 Raises 825 ------ 826 `SSSCoringError` if the `jumpResults` object is empty. 827 """ 828 if not len(jumpResults): 829 raise SSScoringError('jumpResults is empty - impossible to collate angles') 830 angles = pd.DataFrame() 831 for jumpResultIndex in sorted(list(jumpResults.keys())): 832 jumpResult = jumpResults[jumpResultIndex] 833 if jumpResult.status == JumpStatus.OK: 834 t = jumpResult.table 835 finalTime = t.iloc[-1].time 836 t.iloc[-1].time = LAST_TIME_TRANCHE 837 t = pd.pivot_table(t, columns = t.time) 838 d = pd.DataFrame([ jumpResult.score, ], index = [ jumpResultIndex, ], columns = [ 'score', ], dtype = object) 839 for column in t.columns: 840 d[column] = t[column].speedAngle 841 d['finalTime'] = [ finalTime, ] 842 843 if angles.empty: 844 angles = d.copy() 845 else: 846 angles = pd.concat([ angles, d, ]) 847 cols = sorted([ column for column in angles.columns if isinstance(column, float) ]) 848 cols = [ 'score', ]+cols+[ 'finalTime', ] 849 angles = angles[cols] 850 angles = angles.replace(np.nan, 0.0) 851 return angles.sort_index()
Collate all the angles by time from the jumpResults
into a dataframe that
features the jump tag as index, the time tranches and the angles at each
time tranche.
Arguments
jumpResults: dict
A dictionary of jump results, in which each result corresponds to a FlySight
file name. See ssscoring.processAllJumpFiles
for details.
Returns
A dataframe featuring these columns:
- Score
- Angles at 5, 10, 15, 20, and 25 second tranches
- Final time contemplated in the analysis
Raises
SSSCoringError
if the jumpResults
object is empty.
854def totalResultsFrom(aggregate: pd.DataFrame) -> pd.DataFrame: 855 """ 856 Calculates the total and mean speeds for an aggregation of speed jumps. 857 858 Arguments 859 --------- 860 aggregate: pd.DataFrame 861 The aggregate results dataframe resulting from calling `ssscoring.aggregateResults` 862 with valid results. 863 864 Returns 865 ------- 866 A dataframe with one row and two columns: 867 868 - totalSpeed ::= the sum of all speeds in the aggregated results 869 - meanSpeed ::= the mean of all speeds 870 - maxSpeed := the absolute max speed over the speed runs set 871 - meanSpeedSTD := scored speeds standar deviation 872 - maxScore ::= the max score among all the speed scores 873 - maxScoreSTD := the max scores standard deviation 874 875 Raises 876 ------ 877 `AttributeError` if aggregate is an empty dataframe or `None`, or if the 878 `aggregate` dataframe doesn't conform to the output of `ssscoring.aggregateResults`. 879 """ 880 if aggregate is None: 881 raise AttributeError('aggregate dataframe is empty') 882 elif isinstance(aggregate, pd.DataFrame) and not len(aggregate): 883 raise AttributeError('aggregate dataframe is empty') 884 885 totals = pd.DataFrame({ 886 'totalScore': [ round(aggregate.score.sum(), 2), ], 887 'mean': [ round(aggregate.score.mean(), 2), ], 888 'deviation': [ round(aggregate.score.std(), 2), ], 889 'maxScore': [ round(aggregate.score.max(), 2), ], 890 'absMaxSpeed': [ round(aggregate.maxSpeed.max(), 2), ], }, index = [ 'totalSpeed'],) 891 return totals
Calculates the total and mean speeds for an aggregation of speed jumps.
Arguments
aggregate: pd.DataFrame
The aggregate results dataframe resulting from calling ssscoring.aggregateResults
with valid results.
Returns
A dataframe with one row and two columns:
- totalSpeed ::= the sum of all speeds in the aggregated results
- meanSpeed ::= the mean of all speeds
- maxSpeed := the absolute max speed over the speed runs set
- meanSpeedSTD := scored speeds standar deviation
- maxScore ::= the max score among all the speed scores
- maxScoreSTD := the max scores standard deviation
Raises
AttributeError
if aggregate is an empty dataframe or None
, or if the
aggregate
dataframe doesn't conform to the output of ssscoring.aggregateResults
.