|
1 | | -from copy import deepcopy |
2 | 1 | from typing import Dict, List, Optional, Set, Tuple, Union |
| 2 | +from itertools import combinations |
3 | 3 |
|
4 | 4 | import numpy as np |
5 | 5 | from matplotlib import colors, pyplot |
@@ -42,13 +42,12 @@ def sign_array(p_values: Union[List, np.ndarray, DataFrame], alpha: float = 0.05 |
42 | 42 | [ 1, -1, 0], |
43 | 43 | [ 1, 0, -1]]) |
44 | 44 | """ |
45 | | - sig_array = deepcopy(np.array(p_values)) |
46 | | - sig_array[sig_array == 0] = 1e-10 |
47 | | - sig_array[sig_array > alpha] = 0 |
48 | | - sig_array[(sig_array < alpha) & (sig_array > 0)] = 1 |
49 | | - np.fill_diagonal(sig_array, -1) |
50 | | - |
51 | | - return sig_array |
| 45 | + p_values = np.asarray(p_values) |
| 46 | + if (p_values < 0).any(): |
| 47 | + raise ValueError("P values matrix must be non-negative") |
| 48 | + result = (p_values <= alpha).astype(np.int8) # Returns a copy |
| 49 | + np.fill_diagonal(result, -1) |
| 50 | + return result |
52 | 51 |
|
53 | 52 |
|
54 | 53 | def sign_table( |
@@ -518,41 +517,51 @@ def critical_difference_diagram( |
518 | 517 | ranks.iloc[: len(ranks) // 2], |
519 | 518 | ranks.iloc[len(ranks) // 2 :], |
520 | 519 | ) |
521 | | - # points_left, points_right = np.array_split(ranks.sort_values(), 2) |
522 | 520 |
|
523 | 521 | # Sets of points under the same crossbar |
524 | | - crossbar_sets = _find_maximal_cliques(adj_matrix) |
| 522 | + crossbar_sets = [bar for bar in _find_maximal_cliques(adj_matrix) if len(bar) > 1] |
525 | 523 |
|
526 | | - # Sort by lowest rank and filter single-valued sets |
527 | | - crossbar_sets = sorted( |
528 | | - (x for x in crossbar_sets if len(x) > 1), key=lambda x: ranks[list(x)].min() |
529 | | - ) |
| 524 | + if not crossbar_sets: # All points are significantly different |
| 525 | + # The list of crossbars is left empty |
| 526 | + lowest_crossbar_ypos = -1 |
| 527 | + else: |
| 528 | + crossbar_min_max = [ # Will be used to check if two crossbars intersect |
| 529 | + ranks.reindex(bar).agg(["min", "max"]) |
| 530 | + for bar in crossbar_sets |
| 531 | + ] |
| 532 | + |
| 533 | + # Create an adjacency matrix of the crossbars, where 1 means that the two |
| 534 | + # crossbars do not intersect, meaning that they can be plotted on the same |
| 535 | + # level. |
| 536 | + n_bars = len(crossbar_sets) |
| 537 | + on_same_level = DataFrame(True, index=range(n_bars), columns=range(n_bars)) |
| 538 | + |
| 539 | + for (i, bar_i), (j, bar_j) in combinations(enumerate(crossbar_min_max), 2): |
| 540 | + on_same_level.loc[i, j] = on_same_level.loc[j, i] = ( |
| 541 | + (bar_i["max"] < bar_j["min"]) or (bar_i["min"] > bar_j["max"]) |
| 542 | + ) |
530 | 543 |
|
531 | | - # Create stacking of crossbars: for each level, try to fit the crossbar, |
532 | | - # so that it does not intersect with any other in the level. If it does not |
533 | | - # fit in any level, create a new level for it. |
534 | | - crossbar_levels: list[list[set]] = [] |
535 | | - for bar in crossbar_sets: |
| 544 | + # The levels are the maximal cliques of the crossbar adjacency matrix. |
| 545 | + crossbar_levels = _find_maximal_cliques(on_same_level) |
| 546 | + |
| 547 | + # Plot the crossbars in each level |
| 548 | + crossbars = [] |
536 | 549 | for level, bars_in_level in enumerate(crossbar_levels): |
537 | | - if not any(bool(bar & bar_in_lvl) for bar_in_lvl in bars_in_level): |
538 | | - ypos = -level - 1 |
539 | | - bars_in_level.append(bar) |
540 | | - break |
541 | | - else: |
542 | | - ypos = -len(crossbar_levels) - 1 |
543 | | - crossbar_levels.append([bar]) |
544 | | - |
545 | | - crossbars.append( |
546 | | - ax.plot( |
547 | | - # Adding a separate line between each pair enables showing a |
548 | | - # marker over each elbow with crossbar_props={'marker': 'o'}. |
549 | | - [ranks[i] for i in bar], |
550 | | - [ypos] * len(bar), |
551 | | - **crossbar_props, |
552 | | - ) |
553 | | - ) |
| 550 | + plotted_bars_in_level = [] |
| 551 | + for bar_index in bars_in_level: |
| 552 | + bar = crossbar_sets[bar_index] |
| 553 | + plotted_bar, *_ = ax.plot( |
| 554 | + # We could plot a single line segment between min and max. However, |
| 555 | + # adding a separate segment between each pair enables showing a |
| 556 | + # marker over each elbow, e.g. crossbar_props={'marker': 'o'}. |
| 557 | + [ranks[i] for i in bar], |
| 558 | + [-level - 1] * len(bar), |
| 559 | + **crossbar_props, |
| 560 | + ) |
| 561 | + plotted_bars_in_level.append(plotted_bar) |
| 562 | + crossbars.append(plotted_bars_in_level) |
554 | 563 |
|
555 | | - lowest_crossbar_ypos = -len(crossbar_levels) |
| 564 | + lowest_crossbar_ypos = -len(crossbar_levels) |
556 | 565 |
|
557 | 566 | def plot_items(points, xpos, label_fmt, color_palette, label_props): |
558 | 567 | """Plot each marker + elbow + label.""" |
|
0 commit comments