def gen_text_general_info(xml, r): from dateutil.relativedelta import relativedelta total_notes = 0 for tap_note_scores in xml.iter("TapNoteScores"): total_notes += sum(int(e.text) for e in tap_note_scores) total_notes_string = util.abbreviate(total_notes, min_precision=3) scores = list(iter_scores(xml)) num_charts = len(list(xml.iter("Chart"))) hours = sum(float(s.findtext("SurviveSeconds")) / 3600 for s in scores) first_play_date = min([parsedate(s.findtext("DateTime")) for s in scores]) duration = relativedelta(datetime.now(), first_play_date) grades = count_nums_grades(xml) # ~ grades_string_1 = ", ".join(f"{name}: {grades[name]}" for name in ("AAAA", "AAA", "AA")) # ~ grades_string_2 = ", ".join(f"{name}: {grades[name]}" for name in ("A", "B", "C", "D")) grades_string = ", ".join(f"{name}: {grades[name]}" for name in "AAAA AAA AA A B C D".split()) grade_names = list(reversed(util.grade_names)) best_aaa = (None, 0) best_aaaa = (None, 0) for score in iter_scores(xml): wifescore = float(score.findtext("SSRNormPercent")) skillset_ssrs = score.find("SkillsetSSRs") if skillset_ssrs is None: continue overall = float(skillset_ssrs.findtext("Overall")) if wifescore < util.AAA_THRESHOLD: pass # we don't care about sub-AAA scores elif wifescore < util.AAAA_THRESHOLD: if overall > best_aaa[1]: best_aaa = (score, overall) else: if overall > best_aaaa[1]: best_aaaa = (score, overall) def get_score_desc(score, overall) -> str: if score is None: return "[none]" chart = util.find_parent_chart(xml, score) dt = score.findtext("DateTime") wifescore = float(score.findtext("SSRNormPercent")) pack = chart.get("Pack") song = chart.get("Song") return f"{overall:.2f}, {wifescore*100:.2f}% - \"{song}\" ({pack}) - {dt[:10]}" return "<br>".join([ f"You started playing {duration.years} years {duration.months} months ago", f"Total hours spent playing: {round(hours)} hours", f"Number of scores: {len(scores)}", f"Number of unique files played: {num_charts}", f"Grades: {grades_string}", # ~ f"Grades: {grades_string_1}", # ~ f"{util.gen_padding_from('Grades: ')}{grades_string_2}", f"Total arrows hit: {total_notes_string}", f"Best AAA: {get_score_desc(best_aaa[0], best_aaa[1])}", f"Best AAAA: {get_score_desc(best_aaaa[0], best_aaaa[1])}", ])
def gen_week_skillsets(xml): # returns an integer week from 0-51 def week_from_score(score) -> int: datetime = parsedate(score.findtext("DateTime")) week = datetime.isocalendar()[1] return week chronological_scores = sorted(iter_scores(xml), key=lambda s: s.findtext("DateTime")) week_start_datetimes: List[datetime] = [] diffsets: List[List[float]] = [] for week, scores_in_week in util.groupby(chronological_scores, week_from_score): diffset = [0, 0, 0, 0, 0, 0, 0] for score in scores_in_week: skillset_ssrs = score.find("SkillsetSSRs") if skillset_ssrs is None: continue diffs = [float(diff.text) for diff in skillset_ssrs[1:]] main_diff = diffs.index(max(diffs)) diffset[main_diff] += 1 total = sum(diffset) if total == 0: continue diffset = [diff / total * 100 for diff in diffset] year = scores_in_week[0].findtext("DateTime")[:4] week_start_datetime = datetime.strptime(f"{year} {week} {0}", "%Y %W %w") diffsets.append(diffset) week_start_datetimes.append(week_start_datetime) return (week_start_datetimes, diffsets)
def calc_median_score_increase(xml): from statistics import median score_increases = [] for chart in xml.iter("ScoresAt"): # Chronologically sorted scores scores = sorted(iter_scores(chart), key=lambda s: s.findtext("DateTime")) for i in range(0, len(scores) - 1): datetime_1 = parsedate(scores[i].findtext("DateTime")) datetime_2 = parsedate(scores[i + 1].findtext("DateTime")) time_delta = datetime_2 - datetime_1 play_time = float(scores[i].findtext("SurviveSeconds")) idle_time = time_delta.total_seconds() - play_time # If the same chart is played twice within 60 seconds if idle_time < 60: score_1 = float(scores[i].findtext("SSRNormPercent")) score_2 = float(scores[i + 1].findtext("SSRNormPercent")) score_increase = 100 * (score_2 - score_1) score_increases.append(score_increase) if len(score_increases) == 0: return 0 else: return median(score_increases)
def divide_into_sessions(xml): if cache("sessions_division_cache"): return cache("sessions_division_cache") session_end_threshold = timedelta(hours=1) scores = list(iter_scores(xml)) datetimes = [parsedate(s.find("DateTime").text) for s in scores] zipped = zip(scores, datetimes) zipped = sorted(zipped, key=lambda pair: pair[1]) # zipped is a list of chronologically sorted (score object, datetime) tuples prev_score_datetime = zipped[0][1] # first datetime current_session = [ zipped[0] ] # list of (score object, datetime) tuples in current session sessions = [ ] # list of sessions where every session is like `current_session` for score, score_datetime in zipped[1:]: score_interval = score_datetime - prev_score_datetime # check if timedelta between two scores is too high if score_interval > session_end_threshold: sessions.append(current_session) current_session = [] current_session.append((score, score_datetime)) prev_score_datetime = score_datetime sessions.append(current_session) return cache("sessions_division_cache", sessions)
def count_nums_grades(xml): grades = [] for score in util.iter_scores(xml): percent = float(score.findtext("SSRNormPercent")) grade = sum(percent >= t for t in util.grade_thresholds) - 1 grades.append(util.grade_names[grade]) return Counter(grades)
def calc_average_hours_per_day(xml, timespan=timedelta(days=365 / 2)): scores = sorted(iter_scores(xml), key=lambda s: s.findtext("DateTime")) total_hours = 0 for score in scores: total_hours += float(score.findtext("SurviveSeconds")) / 3600 return total_hours / timespan.days
def gen_most_played_charts(xml, num_charts): charts_num_plays = [] for chart in xml.iter("Chart"): score_filter = lambda s: float(s.findtext("SSRNormPercent")) > 0.5 num_plays = len([s for s in iter_scores(chart) if score_filter(s)]) if num_plays > 0: charts_num_plays.append((chart, num_plays)) charts_num_plays.sort(key=lambda pair: pair[1], reverse=True) return charts_num_plays[:num_charts]
def find_longest_combo(xml): max_combo_chart = None max_combo = 0 for chart in xml.iter("Chart"): for score in iter_scores(chart): combo = int(score.findtext("MaxCombo")) if combo > max_combo: max_combo = combo max_combo_chart = chart return max_combo_chart, max_combo
def gen_wifescore_frequencies(xml): # e.g. the 0.70 bucket corresponds to all scores between 0.70 and 0.71 (not 0.695 and 0.705!) frequencies = {percent: 0 for percent in range(70, 100)} for score in iter_scores(xml): wifescore = float(score.findtext("SSRNormPercent")) percent = round(wifescore * 100) if percent in frequencies: frequencies[percent] += 1 return list(frequencies.keys()), list(frequencies.values())
def gen_plays_by_hour(xml): num_plays = [0] * 24 for score in iter_scores(xml): datetime = parsedate(score.find("DateTime").text) num_plays[datetime.hour] += 1 # I tried to use a datetime as key (would be nicer to display), but # it doesn't play nicely with matplotlib, so we need to use an # integer to represent the hour of the day. #return {time(hour=i): num_plays[i] for i in range(24)} return list(range(24)), num_plays
def generate_pack_likings(xml, months): likings = {} for chart in xml.iter("Chart"): num_relevant_plays = 0 for score in iter_scores(chart): if util.score_within_n_months(score, months): num_relevant_plays += 1 pack = chart.get("Pack") if pack not in likings: likings[pack] = 0 likings[pack] += num_relevant_plays return likings
def gen_hours_per_skillset(xml): hours = [0, 0, 0, 0, 0, 0, 0] for score in iter_scores(xml): skillset_ssrs = score.find("SkillsetSSRs") if skillset_ssrs is None: continue diffs = [float(diff.text) for diff in skillset_ssrs[1:]] main_diff = diffs.index(max(diffs)) length_hours = float(score.findtext("SurviveSeconds")) / 3600 hours[main_diff] += length_hours return hours
def calculate_total_wifescore(xml, months=6): weighted_sum = 0 num_notes_sum = 0 for score in iter_scores(xml): if not util.score_within_n_months(score, months): continue num_notes = util.num_notes(score) num_notes_sum += util.num_notes(score) wifescore = float(score.findtext("SSRNormPercent")) weighted_sum += wifescore * num_notes try: return weighted_sum / num_notes_sum except ZeroDivisionError: return 0
def gen_plays_per_week(xml): datetimes = [parsedate(s.findtext("DateTime")) for s in iter_scores(xml)] datetimes.sort() weeks = {} week_end = datetimes[0] week_start = week_end - timedelta(weeks=1) i = 0 while i < len(datetimes): if datetimes[i] < week_end: weeks[week_start] += 1 i += 1 else: week_start += timedelta(weeks=1) week_end += timedelta(weeks=1) weeks[week_start] = 0 return (list(weeks.keys()), list(weeks.values()))
def gen_idle_time_buckets(xml): # Each bucket is 5 seconds. Total 10 minutes is tracked buckets = [0] * 600 a, b = 0, 0 scores = [] for scoresat in xml.iter("ScoresAt"): rate = float(scoresat.get("Rate")) scores.extend(((score, rate) for score in iter_scores(scoresat))) # Sort scores by datetime, oldest first scores.sort(key=lambda pair: pair[0].findtext("DateTime")) last_play_end = None for score, rate in scores: a += 1 datetime = util.parsedate(score.findtext("DateTime")) survive_seconds = float(score.findtext("SurviveSeconds")) #print(survive_seconds, rate) length = timedelta(seconds=survive_seconds * rate) #print("Datetime:", datetime) #print("Play length:", str(length)[:-7], "(according to SurviveSeconds)") if last_play_end is not None: idle_time = datetime - last_play_end if idle_time >= timedelta(): bucket_index = int(idle_time.total_seconds() // 5) if bucket_index < len(buckets): buckets[bucket_index] += 1 else: #print("Negative idle time!") b += 1 last_play_end = datetime + length #print("Finished", last_play_end) #print() # ~ keys = [i * 5 for i in range(len(buckets))] keys = range(len(buckets)) return (keys, buckets)
def gen_hours_per_week(xml): scores = iter_scores(xml) pairs = [(s, parsedate(s.findtext("DateTime"))) for s in scores] pairs.sort(key=lambda pair: pair[1]) # Sort by datetime weeks = {} week_end = pairs[0][1] # First (earliest) datetime week_start = week_end - timedelta(weeks=1) i = 0 while i < len(pairs): score, datetime = pairs[i][0], pairs[i][1] if datetime < week_end: score_seconds = float(score.findtext("SurviveSeconds")) or 0 weeks[week_start] += score_seconds / 3600 i += 1 else: week_start += timedelta(weeks=1) week_end += timedelta(weeks=1) weeks[week_start] = 0 return (list(weeks.keys()), list(weeks.values()))
def map_scores(xml, mapper, *mapper_args, discard_errors=True, brush_color_over_10_notes=None): x, y = [], [] ids = [] if brush_color_over_10_notes: brushes = [] for score in iter_scores(xml): if discard_errors: try: value = (mapper)(score, *mapper_args) except Exception: continue else: value = (mapper)(score, *mapper_args) if value is None: continue x.append(parsedate(score.findtext("DateTime"))) y.append(value) ids.append(score) if brush_color_over_10_notes: tap_note_scores = score.find("TapNoteScores") if tap_note_scores: judgements = ["Miss", "W1", "W2", "W3", "W4", "W5"] total_notes = sum( int(tap_note_scores.findtext(x)) for x in judgements) else: total_notes = 500 # just assume 100 as a default yolo brushes.append( brush_color_over_10_notes if total_notes > 10 else "#AAAAAA") if brush_color_over_10_notes: return (((x, y), ids), brushes) else: return ((x, y), ids)