Beispiel #1
0
    async def extreme(self, ctx, *args: str):
        """Plots pairs of lowest rated unsolved problem and highest rated solved problem for every
        contest that was rated for the given user.
        """
        (solved, unsolved), args = cf_common.filter_flags(args, ['+solved', '+unsolved'])
        if not solved and not unsolved:
            solved = unsolved = True

        handles = args or ('!' + str(ctx.author),)
        handle, = await cf_common.resolve_handles(ctx, self.converter, handles)
        ratingchanges = await cf.user.rating(handle=handle)
        if not ratingchanges:
            raise GraphCogError(f'User {handle} is not rated')

        contest_ids = [change.contestId for change in ratingchanges]
        subs_by_contest_id = {contest_id: [] for contest_id in contest_ids}
        for sub in await cf.user.status(handle=handle):
            if sub.contestId in subs_by_contest_id:
                subs_by_contest_id[sub.contestId].append(sub)

        packed_contest_subs_problemset = [
            (cf_common.cache2.contest_cache.get_contest(contest_id),
             cf_common.cache2.problemset_cache.get_problemset(contest_id),
             subs_by_contest_id[contest_id])
            for contest_id in contest_ids
        ]

        rating = max(ratingchanges, key=lambda change: change.ratingUpdateTimeSeconds).newRating
        _plot_extreme(handle, rating, packed_contest_subs_problemset, solved, unsolved)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Codeforces extremes graph')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #2
0
    async def howgud(self, ctx, *members: discord.Member):
        members = members or (ctx.author, )
        if len(members) > 5:
            raise GraphCogError('Please specify at most 5 gudgitters.')

        # shift the [-300, 300] gitgud range to center the text
        hist_bins = list(range(-300 - 50, 300 + 50 + 1, 100))
        deltas = [[x[0] for x in cf_common.user_db.howgud(member.id)]
                  for member in members]
        labels = [
            gc.StrWrap(f'{member.display_name}: {len(delta)}')
            for member, delta in zip(members, deltas)
        ]

        plt.clf()
        plt.margins(x=0)
        plt.hist(deltas, bins=hist_bins, label=labels, rwidth=1)
        plt.xlabel('Problem delta')
        plt.ylabel('Number solved')
        plt.legend(prop=self.fontprop)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Histogram of gudgitting')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #3
0
    async def rating(self, ctx, *args: str):
        """Plots Codeforces rating graph for the handles provided."""
        (zoom,), args = cf_common.filter_flags(args, ['+zoom'])
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]

        if not any(resp):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} is not rated'
            else:
                message = f'None of the given users {handles_str} are rated'
            raise GraphCogError(message)

        plt.clf()
        _plot_rating(resp)
        current_ratings = [rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp]
        labels = [f'\N{ZERO WIDTH SPACE}{handle} ({rating})' for handle, rating in zip(handles, current_ratings)]
        plt.legend(labels, loc='upper left')

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.newRating)
                    max_rating = max(max_rating, rating.newRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Rating graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #4
0
    async def curve(self, ctx, *args: str):
        """Plots the count of problems solved over time on Codeforces for the handles provided."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [filt.filter_subs(submissions) for submissions in resp]

        if not any(all_solved_subs):
            raise GraphCogError(f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Time')
        plt.ylabel('Cumulative solve count')

        all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_subs]
                     for solved_subs in all_solved_subs]
        for times in all_times:
            cumulative_solve_count = list(range(1, len(times)+1)) + [len(times)]
            timestretched = times + [min(dt.datetime.now(), dt.datetime.fromtimestamp(filt.dhi))]
            plt.plot(timestretched, cumulative_solve_count)

        labels = [gc.StrWrap(f'{handle}: {len(times)}')
                  for handle, times in zip(handles, all_times)]

        plt.legend(labels)

        plt.gcf().autofmt_xdate()
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Curve of number of solved problems over time')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #5
0
    async def scatter(self, ctx, *args):
        """Plot Codeforces rating overlaid on a scatter plot of problems solved.
        Also plots a running average of ratings of problems solved in practice."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handle, bin_size, point_size = None, 10, 3
        for arg in args:
            if arg[0:2] == 'b=':
                bin_size = int(arg[2:])
            elif arg[0:2] == 's=':
                point_size = int(arg[2:])
            else:
                if handle:
                    raise GraphCogError('Only one handle allowed.')
                handle = arg

        if bin_size < 1 or point_size < 1 or point_size > 100:
            raise GraphCogError('Invalid parameters')

        handle = handle or '!' + str(ctx.author)
        handle, = await cf_common.resolve_handles(ctx, self.converter, (handle,))
        rating_resp = [await cf.user.rating(handle=handle)]
        rating_resp = [filt.filter_rating_changes(rating_changes) for rating_changes in rating_resp]
        submissions = filt.filter_subs(await cf.user.status(handle=handle))

        def extract_time_and_rating(submissions):
            return [(dt.datetime.fromtimestamp(sub.creationTimeSeconds), sub.problem.rating)
                    for sub in submissions]

        if not any(submissions):
            raise GraphCogError(f'No submissions for user `{handle}`')

        solved_by_type = _classify_submissions(submissions)
        regular = extract_time_and_rating(solved_by_type['CONTESTANT'] +
                                          solved_by_type['OUT_OF_COMPETITION'])
        practice = extract_time_and_rating(solved_by_type['PRACTICE'])
        virtual = extract_time_and_rating(solved_by_type['VIRTUAL'])

        plt.clf()
        _plot_scatter(regular, practice, virtual, point_size)
        labels = []
        if practice:
            labels.append('Practice')
        if regular:
            labels.append('Regular')
        if virtual:
            labels.append('Virtual')
        plt.legend(labels, loc='upper left')
        _plot_average(practice, bin_size)
        _plot_rating(rating_resp, mark='')

        # zoom
        ymin, ymax = plt.gca().get_ylim()
        plt.ylim(max(ymin, filt.rlo - 100), min(ymax, filt.rhi + 100))

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title=f'Rating vs solved problem rating for {handle}')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #6
0
    async def hist(self, ctx, *args: str):
        """Shows the histogram of problems solved over time on Codeforces for the handles provided."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [
            filt.filter_subs(submissions) for submissions in resp
        ]

        if not any(all_solved_subs):
            raise GraphCogError(
                f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Time')
        plt.ylabel('Number solved')
        if len(handles) == 1:
            handle, solved_by_type = handles[0], _classify_submissions(
                all_solved_subs[0])
            all_times = [[
                dt.datetime.fromtimestamp(sub.creationTimeSeconds)
                for sub in solved_by_type[sub_type]
            ] for sub_type in filt.types]

            nice_names = nice_sub_type(filt.types)
            labels = [
                name.format(len(times))
                for name, times in zip(nice_names, all_times)
            ]
            plt.hist(all_times, stacked=True, label=labels, bins=34)

            total = sum(map(len, all_times))
            plt.legend(title=f'{handle}: {total}',
                       title_fontsize=plt.rcParams['legend.fontsize'])
        else:
            all_times = [[
                dt.datetime.fromtimestamp(sub.creationTimeSeconds)
                for sub in solved_subs
            ] for solved_subs in all_solved_subs]

            # NOTE: matplotlib ignores labels that begin with _
            # https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend
            # Add zero-width space to work around this
            labels = [
                gc.StrWrap(f'{handle}: {len(times)}')
                for handle, times in zip(handles, all_times)
            ]

            plt.hist(all_times)
            plt.legend(labels)

        plt.gcf().autofmt_xdate()
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Histogram of number of solved problems over time')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #7
0
    async def _rating_hist(self, ctx, ratings, mode, binsize, title):
        if mode not in ('log', 'normal'):
            raise GraphCogError('Mode should be either `log` or `normal`')

        ratings = [r for r in ratings if r >= 0]
        assert ratings, 'Cannot histogram plot empty list of ratings'

        assert 100 % binsize == 0  # because bins is semi-hardcoded
        bins = 39*100//binsize

        colors = []
        low, high = 0, binsize * bins
        for rank in cf.RATED_RANKS:
            for r in range(max(rank.low, low), min(rank.high, high), binsize):
                colors.append('#' + '%06x' % rank.color_embed)
        assert len(colors) == bins, f'Expected {bins} colors, got {len(colors)}'

        height = [0] * bins
        for r in ratings:
            height[r // binsize] += 1

        csum = 0
        cent = [0]
        users = sum(height)
        for h in height:
            csum += h
            cent.append(round(100 * csum / users))

        x = [k * binsize for k in range(bins)]
        label = [f'{r} ({c})' for r, c in zip(x, cent)]

        l, r = 0, bins-1
        while not height[l]:
            l += 1
        while not height[r]:
            r -= 1
        x = x[l:r+1]
        cent = cent[l:r+1]
        label = label[l:r+1]
        colors = colors[l:r+1]
        height = height[l:r+1]

        plt.clf()
        fig = plt.figure(figsize=(15, 5))

        plt.xticks(rotation=45)
        plt.xlim(l * binsize - binsize//2, r * binsize + binsize//2)
        plt.bar(x, height, binsize*0.9, color=colors, linewidth=0,
                tick_label=label, log=(mode == 'log'))
        plt.xlabel('Rating')
        plt.ylabel('Number of users')

        discord_file = gc.get_current_figure_as_file()
        plt.close(fig)

        embed = discord_common.cf_color_embed(title=title)
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #8
0
    async def rating(self, ctx, *args: str):
        """Plots Codeforces rating graph for the handles provided."""

        (zoom, peak), args = cf_common.filter_flags(args, ['+zoom' , '+peak'])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]
        resp = [filt.filter_rating_changes(rating_changes) for rating_changes in resp]

        if not any(resp):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} is not rated'
            else:
                message = f'None of the given users {handles_str} are rated'
            raise GraphCogError(message)

        def max_prefix(user):
            max_rate = 0
            res = []
            for data in user:
                old_rating = data.oldRating
                if old_rating == 0:
                    old_rating = 1500
                if data.newRating - old_rating >= 0 and data.newRating >= max_rate:
                    max_rate = data.newRating
                    res.append(data)
            return(res)

        if peak:
            resp = [max_prefix(user) for user in resp]

        plt.clf()
        plt.axes().set_prop_cycle(gc.rating_color_cycler)
        _plot_rating(resp)
        current_ratings = [rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp]
        labels = [gc.StrWrap(f'{handle} ({rating})') for handle, rating in zip(handles, current_ratings)]
        plt.legend(labels, loc='upper left')

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.newRating)
                    max_rating = max(max_rating, rating.newRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Rating graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #9
0
    async def solved(self, ctx, *args: str):
        """Shows a histogram of problems solved on Codeforces for the handles provided.
        e.g. ;plot solved meooow +contest +virtual +outof +dp"""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [filt.filter_subs(
            submissions) for submissions in resp]

        if not any(all_solved_subs):
            raise GraphCogError(f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Problem rating')
        plt.ylabel('Number solved')
        if len(handles) == 1:
            # Display solved problem separately by type for a single user.
            handle, solved_by_type = handles[0], _classify_submissions(
                all_solved_subs[0])
            all_ratings = [[sub.problem.rating for sub in solved_by_type[sub_type]]
                           for sub_type in filt.types]

            nice_names = nice_sub_type(filt.types)
            labels = [name.format(len(ratings))
                      for name, ratings in zip(nice_names, all_ratings)]

            step = 100
            # shift the range to center the text
            hist_bins = list(range(filt.rlo - step // 2,
                                   filt.rhi + step // 2 + 1, step))
            plt.hist(all_ratings, stacked=True, bins=hist_bins, label=labels)
            total = sum(map(len, all_ratings))
            plt.legend(title=f'{handle}: {total}', title_fontsize=plt.rcParams['legend.fontsize'],
                       loc='upper right')

        else:
            all_ratings = [[sub.problem.rating for sub in solved_subs]
                           for solved_subs in all_solved_subs]
            labels = [gc.StrWrap(f'{handle}: {len(ratings)}')
                      for handle, ratings in zip(handles, all_ratings)]

            step = 200
            hist_bins = list(range(filt.rlo - step // 2,
                                   filt.rhi + step // 2 + 1, step))
            plt.hist(all_ratings, bins=hist_bins)
            plt.legend(labels, loc='upper right')

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Histogram of problems solved on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #10
0
    async def performance(self, ctx, *args: str):
        """Plots Codeforces performance graph for the handles provided."""

        (zoom,
         peak), args = cf_common.filter_flags(args,
                                              ['+zoom', '+asdfgdsafefsdve'])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]
        # extract last rating before corrections
        current_ratings = [
            rating_changes[-1].newRating if rating_changes else 'Unrated'
            for rating_changes in resp
        ]
        resp = cf.user.correct_rating_changes(resp=resp)
        resp = [
            filt.filter_rating_changes(rating_changes)
            for rating_changes in resp
        ]

        if not any(resp):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} is not rated'
            else:
                message = f'None of the given users {handles_str} are rated'
            raise GraphCogError(message)

        plt.clf()
        plt.axes().set_prop_cycle(gc.rating_color_cycler)
        _plot_perf(resp)
        labels = [
            gc.StrWrap(f'{handle} ({rating})')
            for handle, rating in zip(handles, current_ratings)
        ]
        plt.legend(labels, loc='upper left')

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.oldRating)
                    max_rating = max(max_rating, rating.oldRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Performance graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #11
0
    async def vcperformance(self, ctx, *members: discord.Member):
        """Plots VC performance for at most 5 users."""
        members = members or (ctx.author, )
        if len(members) > 5:
            raise ContestCogError('Cannot plot more than 5 VCers at once.')
        plot_data = defaultdict(list)

        min_rating = 1100
        max_rating = 1800

        for member in members:
            rating_history = cf_common.user_db.get_vc_rating_history(member.id)
            if not rating_history:
                raise ContestCogError(f'{member.mention} has no vc history.')
            ratingbefore = 1500
            for vc_id, rating in rating_history:
                vc = cf_common.user_db.get_rated_vc(vc_id)
                perf = ratingbefore + (rating - ratingbefore)*4
                date = dt.datetime.fromtimestamp(vc.finish_time)
                plot_data[member.display_name].append((date, perf))
                min_rating = min(min_rating, perf)
                max_rating = max(max_rating, perf)
                ratingbefore = rating

        plt.clf()
        # plot at least from mid gray to mid purple
        for rating_data in plot_data.values():
            x, y = zip(*rating_data)
            plt.plot(x, y,
                     linestyle='-',
                     marker='o',
                     markersize=4,
                     markerfacecolor='white',
                     markeredgewidth=0.5)

        gc.plot_rating_bg(cf.RATED_RANKS)
        plt.gcf().autofmt_xdate()

        plt.ylim(min_rating - 100, max_rating + 200)
        labels = [
            gc.StrWrap('{} ({})'.format(
                member_display_name,
                ratingbefore))
            for member_display_name, rating_data in plot_data.items()
        ]
        plt.legend(labels, loc='upper left', prop=gc.fontprop)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='VC performance graph')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #12
0
    async def extreme(self, ctx, *args: str):
        """Plots Codeforces lowest unsolved problem/highest solved on codeforces graph for the handle provided."""
        handles = args or ('!' + str(ctx.author), )
        if len(handles) > 1:
            raise GraphCogError('Too many users, try one at a time.')

        handles = await cf_common.resolve_handles(ctx, self.converter, handles)

        resp = await cf.user.rating(handle=handles[0])

        if not resp:
            raise GraphCogError('This user is not rated.')

        contests = [
            cf_common.cache2.contest_cache.get_contest(change.contestId)
            for change in resp
        ]
        contests.sort(key=lambda c: c.id)
        contest_ids = set(c.id for c in contests)
        user_status = [
            submission
            for submission in await cf.user.status(handle=handles[0])
            if submission.contestId in contest_ids
        ]
        user_status.sort(key=lambda sub: sub.contestId)

        statuses = []
        for submission in user_status:
            if not statuses or statuses[-1][
                    0].contestId != submission.contestId:
                statuses.append([])
            statuses[-1].append(submission)

        problemsets = [
            problems for problems in [
                cf_common.cache2.contest_cache.get_problems(contest_id=c.id)
                for c in contests
            ]
        ]

        plt.clf()
        user, = await cf.user.info(handles=handles)
        _plot_extreme(user, resp, statuses, problemsets)
        current_rating = resp[-1].newRating
        labels = [f'\N{ZERO WIDTH SPACE}{handles[0]} ({current_rating})']

        discord_file = _get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Extremes graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #13
0
    async def rating(self, ctx, *args: str):
        """Plots Codeforces rating graph for the handles provided."""

        (zoom, ), args = cf_common.filter_flags(args, ["+zoom"])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        args = _mention_to_handle(args, ctx)
        handles = args or ("!" + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]
        resp = [
            filt.filter_rating_changes(rating_changes)
            for rating_changes in resp
        ]

        if not any(resp):
            handles_str = ", ".join(f"`{handle}`" for handle in handles)
            if len(handles) == 1:
                message = f"User {handles_str} is not rated"
            else:
                message = f"None of the given users {handles_str} are rated"
            raise GraphCogError(message)

        plt.clf()
        _plot_rating(resp)
        current_ratings = [
            rating_changes[-1].newRating if rating_changes else "Unrated"
            for rating_changes in resp
        ]
        labels = [
            gc.StrWrap(f"{handle} ({rating})")
            for handle, rating in zip(handles, current_ratings)
        ]
        plt.legend(labels, loc="upper left")

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.newRating)
                    max_rating = max(max_rating, rating.newRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title="Rating graph on Codeforces")
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #14
0
    async def scatter(self, ctx, handle: str = None, bin_size: int = 10):
        """Plot Codeforces rating overlaid on a scatter plot of problems solved.
        Also plots a running average of ratings of problems solved in practice."""
        if bin_size < 1:
            raise GraphCogError(
                'Moving average window size must be at least 1')

        handle = handle or '!' + str(ctx.author)
        handles = await cf_common.resolve_handles(ctx, self.converter,
                                                  (handle, ))
        resp = [await cf.user.status(handle=handle) for handle in handles]
        rating_resp = [
            await cf.user.rating(handle=handle) for handle in handles
        ]
        contests = await cf.contest.list()
        handle = handles[0]
        submissions = resp[0]

        def extract_time_and_rating(submissions):
            return [(dt.datetime.fromtimestamp(sub.creationTimeSeconds),
                     sub.problem.rating) for sub in submissions]

        solved_subs = _filter_solved_submissions(submissions, contests)

        if not any(rating_resp) and not any(solved_subs):
            raise GraphCogError(
                f'User `{handle}` is not rated and has not solved any rated problem'
            )

        solved_by_type = _classify_submissions(solved_subs)
        regular = extract_time_and_rating(solved_by_type['CONTESTANT'] +
                                          solved_by_type['OUT_OF_COMPETITION'])
        practice = extract_time_and_rating(solved_by_type['PRACTICE'])
        virtual = extract_time_and_rating(solved_by_type['VIRTUAL'])

        plt.clf()
        _plot_scatter(regular, practice, virtual)
        labels = ['Practice', 'Regular', 'Virtual']
        plt.legend(labels, loc='upper left')
        _plot_average(practice, bin_size)
        _plot_rating(rating_resp, mark='')

        discord_file = _get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title=f'Rating vs solved problem rating for {handle}')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #15
0
    async def howgud(self, ctx, *members: discord.Member):
        # shift the [-300, 300] gitgud range to center the test
        hist_bins = list(range(-300 + 50, 300 + 50 + 1, 100))
        deltas = [[x[0] for x in cf_common.user_db.howgud(member.id)]
                  for member in members]
        labels = [
            f'\N{ZERO WIDTH SPACE}{member.display_name}: {len(delta)}'
            for member, delta in zip(members, deltas)
        ]

        plt.clf()
        plt.margins(x=0)
        plt.hist(deltas, bins=hist_bins, label=labels, rwidth=1)
        plt.xlabel('Problem delta')
        plt.ylabel('Number solved')
        plt.legend()

        discord_file = _get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Histogram of gudgitting')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #16
0
    async def centile(self, ctx, *args: str):
        """Show percentile distribution of codeforces and mark given handles in the plot. If +zoom and handles are given, it zooms to the neighborhood of the handles."""
        (zoom, nomarker), args = cf_common.filter_flags(args, ['+zoom', '+nomarker'])
        # Prepare data
        intervals = [(rank.low, rank.high) for rank in cf.RATED_RANKS]
        colors = [rank.color_graph for rank in cf.RATED_RANKS]

        ratings = cf_common.cache2.rating_changes_cache.get_all_ratings()
        ratings = np.array(sorted(ratings))
        n = len(ratings)
        perc = 100*np.arange(n)/n

        users_to_mark = {}
        if not nomarker:
            handles = args or ('!' + str(ctx.author),)
            handles = await cf_common.resolve_handles(ctx,
                                                      self.converter,
                                                      handles,
                                                      mincnt=0,
                                                      maxcnt=50)
            infos = await cf.user.info(handles=list(set(handles)))

            for info in infos:
                if info.rating is None:
                    raise GraphCogError(f'User `{info.handle}` is not rated')
                ix = bisect.bisect_left(ratings, info.rating)
                cent = 100*ix/len(ratings)
                users_to_mark[info.handle] = info.rating,cent

        # Plot
        plt.clf()
        fig,ax = plt.subplots(1)
        ax.plot(ratings, perc, color='#00000099')

        plt.xlabel('Rating')
        plt.ylabel('Percentile')

        for pos in ['right','top','bottom','left']:
            ax.spines[pos].set_visible(False)
        ax.tick_params(axis='both', which='both',length=0)

        # Color intervals by rank
        for interval,color in zip(intervals,colors):
            alpha = '99'
            l,r = interval
            col = color + alpha
            rect = patches.Rectangle((l,-50), r-l, 200,
                                     edgecolor='none',
                                     facecolor=col)
            ax.add_patch(rect)

        # Mark users in plot
        for user,point in users_to_mark.items():
            x,y = point
            plt.annotate(user,
                         xy=point,
                         xytext=(0, 0),
                         textcoords='offset points',
                         ha='right',
                         va='bottom')
            plt.plot(*point,
                     marker='o',
                     markersize=5,
                     color='red',
                     markeredgecolor='darkred')

        # Set limits (before drawing tick lines)
        if users_to_mark and zoom:
            xmargin = 50
            ymargin = 5
            xmin = min(point[0] for point in users_to_mark.values())
            xmax = max(point[0] for point in users_to_mark.values())
            ymin = min(point[1] for point in users_to_mark.values())
            ymax = max(point[1] for point in users_to_mark.values())
            plt.xlim(xmin - xmargin, xmax + xmargin)
            plt.ylim(ymin - ymargin, ymax + ymargin)
        else:
            plt.xlim(ratings[0], ratings[-1])
            plt.ylim(-1.5, 101.5)

        # Draw tick lines
        linecolor = '#00000022'
        inf = 10000
        def horz_line(y):
            l = mlines.Line2D([-inf,inf], [y,y], color=linecolor)
            ax.add_line(l)
        def vert_line(x):
            l = mlines.Line2D([x,x], [-inf,inf], color=linecolor)
            ax.add_line(l)
        for y in ax.get_yticks():
            horz_line(y)
        for x in ax.get_xticks():
            vert_line(x)

        # Discord stuff
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title=f'Rating/percentile relationship')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #17
0
    async def scatter(self, ctx, *args):
        """Plot Codeforces rating overlaid on a scatter plot of problems solved.
        Also plots a running average of ratings of problems solved in practice."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handle, bin_size, point_size = None, 10, 3
        for arg in args:
            if arg[0:2] == "b=":
                bin_size = int(arg[2:])
            elif arg[0:2] == "s=":
                point_size = int(arg[2:])
            else:
                handle = arg

        if bin_size < 1 or point_size < 1 or point_size > 100:
            raise GraphCogError("Invalid parameters")

        handle = handle or "!" + str(ctx.author)
        (handle, ) = await cf_common.resolve_handles(ctx, self.converter,
                                                     (handle, ))
        rating_resp = [await cf.user.rating(handle=handle)]
        rating_resp = [
            filt.filter_rating_changes(rating_changes)
            for rating_changes in rating_resp
        ]
        submissions = filt.filter_subs(await cf.user.status(handle=handle))

        def extract_time_and_rating(submissions):
            return [(
                dt.datetime.fromtimestamp(sub.creationTimeSeconds),
                sub.problem.rating,
            ) for sub in submissions]

        if not any(rating_resp) and not any(submissions):
            raise GraphCogError(
                f"User `{handle}` is not rated and has not solved any rated problem"
            )

        solved_by_type = _classify_submissions(submissions)
        regular = extract_time_and_rating(solved_by_type["CONTESTANT"] +
                                          solved_by_type["OUT_OF_COMPETITION"])
        practice = extract_time_and_rating(solved_by_type["PRACTICE"])
        virtual = extract_time_and_rating(solved_by_type["VIRTUAL"])

        plt.clf()
        _plot_scatter(regular, practice, virtual, point_size)
        labels = []
        if practice:
            labels.append("Practice")
        if regular:
            labels.append("Regular")
        if virtual:
            labels.append("Virtual")
        plt.legend(labels, loc="upper left")
        _plot_average(practice, bin_size)
        _plot_rating(rating_resp, mark="")

        # zoom
        ymin, ymax = plt.gca().get_ylim()
        plt.ylim(max(ymin, filt.rlo - 100), min(ymax, filt.rhi + 100))

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title=f"Rating vs solved problem rating for {handle}")
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #18
0
    async def speed(self, ctx, *args):
        """Plot time spent on problems of particular rating during contest."""

        (add_scatter,
         use_median), args = cf_common.filter_flags(args,
                                                    ['+scatter', '+median'])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        if 'PRACTICE' in filt.types:
            filt.types.remove(
                'PRACTICE')  # can't estimate time for practice submissions

        handles, point_size = [], 3
        for arg in args:
            if arg[0:2] == 's=':
                point_size = int(arg[2:])
            else:
                handles.append(arg)

        handles = handles or ['!' + str(ctx.author)]
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [
            filt.filter_subs(submissions) for submissions in resp
        ]

        plt.clf()
        plt.xlabel('Rating')
        plt.ylabel('Minutes spent')

        max_time = 0  # for ylim

        for submissions in all_solved_subs:
            scatter_points = []  # only matters if +scatter

            solved_by_contest = collections.defaultdict(lambda: [])
            for submission in submissions:
                # [solve_time, problem rating, problem index] for each solved problem
                solved_by_contest[submission.contestId].append([
                    submission.relativeTimeSeconds, submission.problem.rating,
                    submission.problem.index
                ])

            time_by_rating = collections.defaultdict(lambda: [])
            for events in solved_by_contest.values():
                events.sort()
                solved_subproblems = dict()
                last_ac_time = 0

                for (current_ac_time, rating, problem_index) in events:
                    time_to_solve = current_ac_time - last_ac_time
                    last_ac_time = current_ac_time

                    # if there are subproblems, add total time for previous subproblems to current one
                    if len(problem_index) == 2 and problem_index[1].isdigit():
                        time_to_solve += solved_subproblems.get(
                            problem_index[0], 0)
                        solved_subproblems[problem_index[0]] = time_to_solve

                    time_by_rating[rating].append(time_to_solve /
                                                  60)  # in minutes

            for rating in time_by_rating.keys():
                times = time_by_rating[rating]
                if use_median:
                    time_by_rating[rating] = np.median(times)
                else:
                    time_by_rating[rating] = sum(times) / len(times)

                if add_scatter:
                    for t in times:
                        scatter_points.append([rating, t])
                        max_time = max(max_time, t)

            xs = sorted(time_by_rating.keys())
            ys = [time_by_rating[rating] for rating in xs]

            max_time = max(max_time, max(ys, default=0))
            plt.plot(xs, ys)
            if add_scatter:
                plt.scatter(*zip(*scatter_points), s=point_size)

        labels = [gc.StrWrap(handle) for handle in handles]
        plt.legend(labels)
        plt.ylim(0, max_time + 5)

        # make xticks divisible by 100
        ticks = plt.gca().get_xticks()
        base = ticks[1] - ticks[0]
        plt.gca().get_xaxis().set_major_locator(
            MultipleLocator(base=max(base // 100 * 100, 100)))
        discord_file = gc.get_current_figure_as_file()
        title = f'Plot of {"median" if use_median else "average"} time spent on a problem'
        embed = discord_common.cf_color_embed(title=title)
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)

        await ctx.send(embed=embed, file=discord_file)
Beispiel #19
0
    async def hist(self, ctx, *args: str):
        """Shows the histogram of problems solved on Codeforces over time for the handles provided"""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        phase_days = 1
        handles = []
        for arg in args:
            if arg[0:11] == 'phase_days=':
                phase_days = int(arg[11:])
            else:
                handles.append(arg)

        if phase_days < 1:
            raise GraphCogError('Invalid parameters')
        phase_time = dt.timedelta(days=phase_days)

        handles = handles or ['!' + str(ctx.author)]
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [filt.filter_subs(submissions) for submissions in resp]

        if not any(all_solved_subs):
            raise GraphCogError(f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Time')
        plt.ylabel('Number solved')
        if len(handles) == 1:
            handle, solved_by_type = handles[0], _classify_submissions(all_solved_subs[0])
            all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_by_type[sub_type]]
                         for sub_type in filt.types]

            nice_names = nice_sub_type(filt.types)
            labels = [name.format(len(times)) for name, times in zip(nice_names, all_times)]

            dlo = min(itertools.chain.from_iterable(all_times)).date()
            dhi = min(dt.datetime.today() + dt.timedelta(days=1), dt.datetime.fromtimestamp(filt.dhi)).date()
            phase_cnt = math.ceil((dhi - dlo) / phase_time)
            plt.hist(
                all_times,
                stacked=True,
                label=labels,
                range=(dhi - phase_cnt * phase_time, dhi),
                bins=min(40, phase_cnt))

            total = sum(map(len, all_times))
            plt.legend(title=f'{handle}: {total}', title_fontsize=plt.rcParams['legend.fontsize'])
        else:
            all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_subs]
                         for solved_subs in all_solved_subs]

            # NOTE: matplotlib ignores labels that begin with _
            # https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend
            # Add zero-width space to work around this
            labels = [gc.StrWrap(f'{handle}: {len(times)}')
                      for handle, times in zip(handles, all_times)]

            dlo = min(itertools.chain.from_iterable(all_times)).date()
            dhi = min(dt.datetime.today() + dt.timedelta(days=1), dt.datetime.fromtimestamp(filt.dhi)).date()
            phase_cnt = math.ceil((dhi - dlo) / phase_time)
            plt.hist(
                all_times,
                range=(dhi - phase_cnt * phase_time, dhi),
                bins=min(40 // len(handles), phase_cnt))
            plt.legend(labels)

        # NOTE: In case of nested list, matplotlib decides type using 1st sublist,
        # it assumes float when 1st sublist is empty.
        # Hence explicitly assigning locator and formatter is must here.
        locator = mdates.AutoDateLocator()
        plt.gca().xaxis.set_major_locator(locator)
        plt.gca().xaxis.set_major_formatter(mdates.AutoDateFormatter(locator))

        plt.gcf().autofmt_xdate()
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Histogram of number of solved problems over time')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #20
0
    async def visualrank(self, ctx, contest_id: int, *args: str):
        """Plot rating changes by rank. Add handles to specify a handle in the plot.
        if arguments contains `+server`, it will include just server members and not all codeforces users.
        Specify `+zoom` to zoom to the neighborhood of handles."""

        args = set(args)
        (in_server,
         zoom), handles = cf_common.filter_flags(args, ["+server", "+zoom"])
        handles = await cf_common.resolve_handles(ctx,
                                                  self.converter,
                                                  handles,
                                                  mincnt=0,
                                                  maxcnt=20)

        users = cf_common.cache2.rating_changes_cache.get_rating_changes_for_contest(
            contest_id)

        if not users:
            raise GraphCogError(
                f"No rating change cache for contest `{contest_id}`")

        if in_server:
            guild_handles = [
                handle for discord_id, handle in
                cf_common.user_db.get_handles_for_guild(ctx.guild.id)
            ]
            users = [user for user in users if user.handle in guild_handles]

        ranks = []
        delta = []
        color = []
        users_to_mark = dict()

        for user in users:
            user_delta = user.newRating - user.oldRating

            ranks.append(user.rank)
            delta.append(user_delta)
            color.append(cf.rating2rank(user.oldRating).color_graph)

            if user.handle in handles:
                users_to_mark[user.handle] = (user.rank, user_delta)

        title = users[0].contestName

        plt.clf()
        fig = plt.figure(figsize=(12, 8))
        plt.title(title)
        plt.xlabel("Rank")
        plt.ylabel("Rating Changes")

        ymargin = 50
        xmargin = 50
        if users_to_mark and zoom:
            xmin = min(point[0] for point in users_to_mark.values())
            xmax = max(point[0] for point in users_to_mark.values())
            ymin = min(point[1] for point in users_to_mark.values())
            ymax = max(point[1] for point in users_to_mark.values())
            mark_size = 2e4 / (xmax - xmin + 2 * xmargin)

            plt.xlim(xmin - xmargin, xmax + xmargin)
            plt.ylim(ymin - ymargin, ymax + ymargin)
        else:
            ylim = 0
            if users_to_mark:
                ylim = max(abs(point[1]) for point in users_to_mark.values())
            ylim = max(ylim, 200)
            xmax = max(user.rank for user in users)
            mark_size = 2e4 / (xmax + 2 * xmargin)

            plt.xlim(-xmargin, xmax + xmargin)
            plt.ylim(-ylim - ymargin, ylim + ymargin)

        plt.scatter(ranks, delta, s=mark_size, c=color)

        for handle, point in users_to_mark.items():
            plt.annotate(
                handle,
                xy=point,
                xytext=(0, 0),
                textcoords="offset points",
                ha="left",
                va="bottom",
                fontsize="large",
            )
            plt.plot(*point, marker="o", markersize=5, color="black")

        discord_file = gc.get_current_figure_as_file()
        plt.close(fig)

        embed = discord_common.cf_color_embed(title=title)
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #21
0
    async def country(self, ctx, *countries):
        """Plots distribution of server members by countries. When no countries are specified, plots
         a bar graph of all members by country. When one or more countries are specified, plots a
         swarmplot of members by country and rating. Only members with registered handles and
         countries set on Codeforces are considered.
         """
        max_countries = 8
        if len(countries) > max_countries:
            raise GraphCogError(f'At most {max_countries} countries may be specified.')

        users = cf_common.user_db.get_cf_users_for_guild(ctx.guild.id)
        counter = collections.Counter(user.country for _, user in users if user.country)

        if not countries:
            # list because seaborn complains for tuple.
            countries, counts = map(list, zip(*counter.most_common()))
            plt.clf()
            fig = plt.figure(figsize=(15, 5))
            with sns.axes_style(rc={'xtick.bottom': True}):
                sns.barplot(x=countries, y=counts)

            # Show counts on top of bars.
            ax = plt.gca()
            for p in ax.patches:
                x = p.get_x() + p.get_width() / 2
                y = p.get_y() + p.get_height() + 0.5
                ax.text(x, y, int(p.get_height()), horizontalalignment='center', color='#30304f',
                        fontsize='x-small')

            plt.xticks(rotation=40, horizontalalignment='right')
            ax.tick_params(axis='x', length=4, color=ax.spines['bottom'].get_edgecolor())
            plt.xlabel('Country')
            plt.ylabel('Number of members')
            discord_file = gc.get_current_figure_as_file()
            plt.close(fig)
            embed = discord_common.cf_color_embed(title='Distribution of server members by country')
        else:
            countries = [country.title() for country in countries]
            data = [[user.country, user.rating]
                    for _, user in users if user.rating and user.country and user.country in countries]
            if not data:
                raise GraphCogError('No rated members from the specified countries are present.')

            color_map = {rating: f'#{cf.rating2rank(rating).color_embed:06x}' for _, rating in data}
            df = pd.DataFrame(data, columns=['Country', 'Rating'])
            column_order = sorted((country for country in countries if counter[country]),
                                  key=counter.get, reverse=True)
            plt.clf()
            if len(column_order) <= 5:
                sns.swarmplot(x='Country', y='Rating', hue='Rating', data=df, order=column_order,
                              palette=color_map)
            else:
                # Add ticks and rotate tick labels to avoid overlap.
                with sns.axes_style(rc={'xtick.bottom': True}):
                    sns.swarmplot(x='Country', y='Rating', hue='Rating', data=df,
                                  order=column_order, palette=color_map)
                plt.xticks(rotation=30, horizontalalignment='right')
                ax = plt.gca()
                ax.tick_params(axis='x', color=ax.spines['bottom'].get_edgecolor())
            plt.legend().remove()
            plt.xlabel('Country')
            plt.ylabel('Rating')
            discord_file = gc.get_current_figure_as_file()
            embed = discord_common.cf_color_embed(title='Rating distribution of server members by '
                                                        'country')

        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #22
0
    async def rating(self, ctx, *members: discord.Member):
        """Plot duelist's rating."""
        members = members or (ctx.author, )
        if len(members) > 5:
            raise DuelCogError(f'Cannot plot more than 5 duelists at once.')

        duelists = [member.id for member in members]
        duels = cf_common.user_db.get_complete_official_duels()
        rating = dict()
        plot_data = defaultdict(list)
        time_tick = 0
        for challenger, challengee, winner, finish_time in duels:
            challenger_r = rating.get(challenger, 1500)
            challengee_r = rating.get(challengee, 1500)
            if winner == Winner.CHALLENGER:
                delta = round(elo_delta(challenger_r, challengee_r, 1))
            elif winner == Winner.CHALLENGEE:
                delta = round(elo_delta(challenger_r, challengee_r, 0))
            else:
                delta = round(elo_delta(challenger_r, challengee_r, 0.5))

            rating[challenger] = challenger_r + delta
            rating[challengee] = challengee_r - delta
            if challenger in duelists or challengee in duelists:
                if challenger in duelists:
                    plot_data[challenger].append(
                        (time_tick, rating[challenger]))
                if challengee in duelists:
                    plot_data[challengee].append(
                        (time_tick, rating[challengee]))
                time_tick += 1

        if time_tick == 0:
            raise DuelCogError(f'Nothing to plot.')

        plt.clf()
        # plot at least from mid gray to mid purple
        min_rating = 1350
        max_rating = 1550
        for rating_data in plot_data.values():
            for tick, rating in rating_data:
                min_rating = min(min_rating, rating)
                max_rating = max(max_rating, rating)

            x, y = zip(*rating_data)
            plt.plot(x,
                     y,
                     linestyle='-',
                     marker='o',
                     markersize=2,
                     markerfacecolor='white',
                     markeredgewidth=0.5)

        gc.plot_rating_bg(DUEL_RANKS)
        plt.xlim(0, time_tick - 1)
        plt.ylim(min_rating - 100, max_rating + 100)

        labels = [
            gc.StrWrap('{} ({})'.format(
                ctx.guild.get_member(duelist).display_name,
                rating_data[-1][1]))
            for duelist, rating_data in plot_data.items()
        ]
        plt.legend(labels, loc='upper left', prop=gc.fontprop)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Duel rating graph')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #23
0
    async def cstatus(self, ctx, contest_id: int):
        """Shows # vs rating graph for the contest with given contest id."""

        contest = cf_common.cache2.contest_cache.get_contest(contest_id)
        wait_msg = None
        if contest.phase != 'BEFORE':
            raise ContestCogError(
                f'Contest `{contest.id} | {contest.name}` is not a future contest'
            )
        wait_msg = await ctx.send('Please wait...')

        self.logger.info(f'Querying Atlas MongoDB')

        registrants = []
        with pymongo.MongoClient(environ.get('DB_STR')) as myclient:
            mydb = myclient["contestPredictionData"]
            if str(contest_id) in mydb.collection_names():
                mycol = mydb[str(contest_id)]
                registrants = mycol.find()

        if registrants == []:
            if wait_msg:
                try:
                    await wait_msg.delete()
                except:
                    pass
            raise ContestCogError(
                f"Registration for contest `{contest.id} | {contest.name}` hasn't started"
            )

        rating_count = {}
        for registrant in registrants:
            rating = int(registrant["rating"])
            if rating in rating_count:
                rating_count[rating] += 1
            else:
                rating_count[rating] = 1

        divcnt = [0, 0, 0]
        for rating in rating_count:
            divcnt[0 if rating < 1600 else
                   2 if rating >= 2100 else 1] += rating_count[rating]

        ratings = [
            rating for rating in list(rating_count.keys()) if rating >= 500
        ]
        ratings.sort()
        counts = [rating_count[rating] for rating in ratings]
        colors = []
        for rating in ratings:
            for col in color_map:
                if rating >= col[0] and rating <= col[1]:
                    colors.append(col[2])
                    break

        plt.clf()
        plt.xlabel('Rating')
        plt.ylabel('#')
        barlist = plt.bar(ratings, counts)
        for bar in barlist:
            rating = float(bar.get_xy()[0])
            for col in color_map:
                if rating >= col[0] and rating <= col[1]:
                    bar.set_color(col[2])
                    break
        plt.text(0.7,
                 1,
                 'Total = ' + str(sum(divcnt)) + '\nDiv1 = ' + str(divcnt[2]) +
                 '\nDiv2 = ' + str(divcnt[1]) + '\nDiv3 = ' + str(divcnt[0]),
                 horizontalalignment='left',
                 verticalalignment='center',
                 transform=plt.gca().transAxes)
        plt.gcf().autofmt_xdate()
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Graph of # vs Rating for contest with id = ' +
            str(contest_id))
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)

        if wait_msg:
            try:
                await wait_msg.delete()
            except:
                pass

        await ctx.send(embed=embed, file=discord_file)
Beispiel #24
0
    async def visualrank(self, ctx, contest_id: int, *args: str):
        """Plot rating changes by rank. Add handles to specify a handle in the plot.
        if arguments contains `+server`, it will include just server members and not all codeforces users.
        Specify `+zoom` to zoom to the neighborhood of handles."""

        args = set(args)
        (in_server, zoom), handles = cf_common.filter_flags(args, ['+server', '+zoom'])
        handles = await cf_common.resolve_handles(ctx, self.converter, handles, mincnt=0, maxcnt=20)

        rating_changes = await cf.contest.ratingChanges(contest_id=contest_id)
        if in_server:
            guild_handles = set(handle for discord_id, handle
                                in cf_common.user_db.get_handles_for_guild(ctx.guild.id))
            rating_changes = [rating_change for rating_change in rating_changes
                              if rating_change.handle in guild_handles or rating_change.handle in handles]

        if not rating_changes:
            raise GraphCogError(f'No rating changes for contest `{contest_id}`')

        users_to_mark = {}
        for rating_change in rating_changes:
            user_delta = rating_change.newRating - rating_change.oldRating
            if rating_change.handle in handles:
                users_to_mark[rating_change.handle] = (rating_change.rank, user_delta)

        ymargin = 50
        xmargin = 50
        if users_to_mark and zoom:
            xmin = min(point[0] for point in users_to_mark.values())
            xmax = max(point[0] for point in users_to_mark.values())
            ymin = min(point[1] for point in users_to_mark.values())
            ymax = max(point[1] for point in users_to_mark.values())
        else:
            ylim = 0
            if users_to_mark:
                ylim = max(abs(point[1]) for point in users_to_mark.values())
            ylim = max(ylim, 200)

            xmin = 0
            xmax = max(rating_change.rank for rating_change in rating_changes)
            ymin = -ylim
            ymax = ylim

        ranks = []
        delta = []
        color = []
        for rating_change in rating_changes:
            user_delta = rating_change.newRating - rating_change.oldRating

            if (xmin - xmargin <= rating_change.rank <= xmax + xmargin
                    and ymin - ymargin <= user_delta <= ymax + ymargin):
                ranks.append(rating_change.rank)
                delta.append(user_delta)
                color.append(cf.rating2rank(rating_change.oldRating).color_graph)

        title = rating_changes[0].contestName

        plt.clf()
        fig = plt.figure(figsize=(12, 8))
        plt.title(title)
        plt.xlabel('Rank')
        plt.ylabel('Rating Changes')

        mark_size = 2e4 / len(ranks)
        plt.xlim(xmin - xmargin, xmax + xmargin)
        plt.ylim(ymin - ymargin, ymax + ymargin)
        plt.scatter(ranks, delta, s=mark_size, c=color)

        for handle, point in users_to_mark.items():
            plt.annotate(handle,
                         xy=point,
                         xytext=(0, 0),
                         textcoords='offset points',
                         ha='left',
                         va='bottom',
                         fontsize='large')
            plt.plot(*point,
                     marker='o',
                     markersize=5,
                     color='black')

        discord_file = gc.get_current_figure_as_file()
        plt.close(fig)

        embed = discord_common.cf_color_embed(title=title)
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Beispiel #25
0
    async def solved(self, ctx, *args: str):
        """Shows a histogram of problems solved on Codeforces for the handles provided."""
        handles = []
        tags = []

        for arg in args:
            if arg[0] == '+':
                tags.append(arg[1:])
            else:
                handles.append(arg)

        handles = handles or ('!' + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        contests = await cf.contest.list()

        all_solved_subs = [
            _filter_solved_submissions(submissions, contests, tags)
            for submissions in resp
        ]

        if not any(all_solved_subs):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} has not solved any rated problem'
            else:
                message = f'None of the users {handles_str} have solved any rated problem'
            raise GraphCogError(message)

        if len(handles) == 1:
            # Display solved problem separately by type for a single user.
            handle, solved_by_type = handles[0], _classify_submissions(
                all_solved_subs[0])

            types_to_show = [
                'CONTESTANT', 'OUT_OF_COMPETITION', 'VIRTUAL', 'PRACTICE'
            ]
            all_ratings = [[
                sub.problem.rating for sub in solved_by_type[sub_type]
            ] for sub_type in types_to_show]
            nice_names = [
                'Contest: {}', 'Unofficial: {}', 'Virtual: {}', 'Practice: {}'
            ]
            labels = [
                name.format(len(ratings))
                for name, ratings in zip(nice_names, all_ratings)
            ]
            total = sum(map(len, all_ratings))

            step = 100
            hist_bins = list(range(500, 3800 + step, step))
            plt.clf()
            plt.hist(all_ratings, stacked=True, bins=hist_bins, label=labels)
            plt.xlabel('Problem rating')
            plt.ylabel('Number solved')
            plt.legend(title=f'{handle}: {total}',
                       title_fontsize=plt.rcParams['legend.fontsize'],
                       loc='upper right')

        else:
            all_ratings = [[sub.problem.rating for sub in solved_subs]
                           for solved_subs in all_solved_subs]

            # NOTE: matplotlib ignores labels that begin with _
            # https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend
            # Add zero-width space to work around this
            labels = [
                f'\N{ZERO WIDTH SPACE}{handle}: {len(ratings)}'
                for handle, ratings in zip(handles, all_ratings)
            ]

            step = 200
            hist_bins = list(range(500, 3800 + step, step))
            plt.clf()
            plt.hist(all_ratings, bins=hist_bins, label=labels)
            plt.xlabel('Problem rating')
            plt.ylabel('Number solved')
            plt.legend(loc='upper right')

        discord_file = _get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Histogram of problems solved on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)