Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added some features for CD diagram personalization #26

Open
msotocalvo opened this issue May 7, 2024 · 2 comments
Open

Added some features for CD diagram personalization #26

msotocalvo opened this issue May 7, 2024 · 2 comments

Comments

@msotocalvo
Copy link

Thank you for the great development you have done, this helps a lot! However, I made some modifications to the source code to give the user the ability to do some customizations to the CD diagram such as: Title, Font Size, Line Width and labels with the individual rank's values.

def cd_diagram(result, reverse, ax, width, fontsize=14, title="Critical Difference Diagram", linewidth=1.5):
"""
Creates a Critical Difference diagram with adjustable font size, title, and line thickness.
"""

def plot_line(line, color='k', **kwargs):
    kwargs.setdefault('linewidth', linewidth)
    ax.plot([pos[0] / width for pos in line], [pos[1] / height for pos in line], color=color, **kwargs)

def plot_text(x, y, s, *args, **kwargs):
    kwargs['fontsize'] = fontsize
    ax.text(x / width, y / height, s, *args, **kwargs)

result_copy = RankResult(**result._asdict())
result_copy = result_copy._replace(rankdf=result.rankdf.sort_values(by='meanrank'))
sorted_ranks, names, groups = get_sorted_rank_groups(result_copy, reverse)
cd = result.cd

lowv = min(1, int(math.floor(min(sorted_ranks))))
highv = max(len(sorted_ranks), int(math.ceil(max(sorted_ranks))))
cline = 0.4
textspace = 1
scalewidth = width - 2 * textspace

def rankpos(rank):
    if not reverse:
        relative_rank = rank - lowv
    else:
        relative_rank = highv - rank
    return textspace + scalewidth / (highv - lowv) * relative_rank

linesblank = 0.2 + 0.2 + (len(groups) - 1) * 0.1

# Add scale
distanceh = 0.25
cline += distanceh

# Calculate height needed for the image
minnotsignificant = max(2 * 0.2, linesblank)
height = cline + ((len(sorted_ranks) + 1) / 2) * 0.2 + minnotsignificant

if ax is None:
    fig, ax = plt.subplots(figsize=(width, height))
    fig.set_facecolor('white')
ax.set_axis_off()

# Upper left corner is (0,0)
ax.plot([0, 1], [0, 1], c="w")
ax.set_xlim(0, 1)
ax.set_ylim(1, 0)

plot_line([(textspace, cline), (width - textspace, cline)], linewidth=linewidth)

bigtick = 0.1
smalltick = 0.05

tick = None
for a in list(np.arange(lowv, highv, 0.5)) + [highv]:
    tick = smalltick
    if a == int(a):
        tick = bigtick
    plot_line([(rankpos(a), cline - tick / 2),
               (rankpos(a), cline)],
              linewidth=linewidth)

for a in range(lowv, highv + 1):
    plot_text(rankpos(a), cline - tick / 2 - 0.05, str(a),
              ha="center", va="bottom")

# Separate rank labels before population names
for i in range(math.ceil(len(sorted_ranks) / 2)):
    chei = cline + minnotsignificant + i * 0.2
    rank_label = f"{sorted_ranks[i]:.2f}"
    plot_line([(rankpos(sorted_ranks[i]), cline),
               (rankpos(sorted_ranks[i]), chei),
               (textspace - 0.1, chei)],
              linewidth=linewidth)
    plot_text(textspace + 0.2, chei - 0.06, rank_label, ha="right", va="center")
    plot_text(textspace - 0.2, chei, names[i], ha="right", va="center")

for i in range(math.ceil(len(sorted_ranks) / 2), len(sorted_ranks)):
    chei = cline + minnotsignificant + (len(sorted_ranks) - i - 1) * 0.2
    rank_label = f"{sorted_ranks[i]:.2f}"
    plot_line([(rankpos(sorted_ranks[i]), cline),
               (rankpos(sorted_ranks[i]), chei),
               (textspace + scalewidth + 0.1, chei)],
              linewidth=linewidth)
    plot_text(textspace + (scalewidth - 0.2), chei - 0.06, rank_label, ha="left", va="center")
    plot_text(textspace + scalewidth + 0.2, chei, names[i], ha="left", va="center")

# Upper scale
if not reverse:
    begin, end = rankpos(lowv), rankpos(lowv + cd)
else:
    begin, end = rankpos(highv), rankpos(highv - cd)

plot_line([(begin, distanceh), (end, distanceh)], linewidth=linewidth)
plot_line([(begin, distanceh + bigtick / 2),
           (begin, distanceh - bigtick / 2)],
          linewidth=linewidth)
plot_line([(end, distanceh + bigtick / 2),
           (end, distanceh - bigtick / 2)],
          linewidth=linewidth)
plot_text((begin + end) / 2, distanceh - 0.05, "CD",
          ha="center", va="bottom")

# No-significance lines
side = 0.05
no_sig_height = 0.1
start = cline + 0.2
for l, r in groups:
    plot_line([(rankpos(sorted_ranks[l]) - side, start),
               (rankpos(sorted_ranks[r]) + side, start)],
              linewidth=linewidth * 2)
    start += no_sig_height

# Add title to the diagram
ax.set_title(title, fontsize=fontsize + 2)

return ax

def plot_stats(result, *, allow_insignificant=False, ax=None, width=None, fontsize=18, title='', linewidth = 1.5):
"""
Creates a plot that supports the analysis of the results of the statistical test.

...

fontsize (int, default=14):
    Font size to be used in the plot.
"""
if not isinstance(result, RankResult):
    raise TypeError("result must be of type RankResult and should be the outcome of calling the autorank function.")

if result.omnibus == 'bayes':
    raise ValueError("plotting results of bayesian analysis not yet supported.")

if result.pvalue >= result.alpha and not allow_insignificant:
    raise ValueError(
        "result is not significant and results of the plot may be misleading. If you want to create the plot "
        "regardless, use the allow_insignificant parameter to suppress this exception.")

if ax is not None and width is not None:
    warnings.warn('width may be ignored because ax is defined.')
if width is None:
    width = 6

if result.omnibus == 'ttest':
    ax = ci_plot(result, True, ax, width, fontsize)
elif result.omnibus == 'wilcoxon':
    warnings.warn('No plot to visualize statistics for Wilcoxon test available. Doing nothing.')
elif result.posthoc == 'tukeyhsd':
    ax = ci_plot(result, True, ax, width, fontsize)
elif result.posthoc == 'nemenyi':
    ax = cd_diagram(result, True, ax, width, fontsize, title, linewidth)
return ax

Regards

@sherbold
Copy link
Owner

sherbold commented May 7, 2024

Looks interesting! Thanks for your contribution.

Could you create a PR for this? Then I can see the diff directly and review this easier.

@msotocalvo
Copy link
Author

Sure, please to help. The PR has been created as 'Solution for issue #26'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants