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