upsetplot-bombcell 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doc/conf.py +289 -0
- examples/plot_customize_after_plot.py +22 -0
- examples/plot_diabetes.py +77 -0
- examples/plot_discrete.py +39 -0
- examples/plot_generated.py +52 -0
- examples/plot_hide.py +42 -0
- examples/plot_highlight.py +75 -0
- examples/plot_highlight_categories.py +41 -0
- examples/plot_missingness.py +23 -0
- examples/plot_sizing.py +49 -0
- examples/plot_theming.py +82 -0
- examples/plot_vertical.py +28 -0
- upsetplot/__init__.py +24 -0
- upsetplot/data.py +420 -0
- upsetplot/plotting.py +1158 -0
- upsetplot/reformat.py +440 -0
- upsetplot/tests/__init__.py +0 -0
- upsetplot/tests/test_data.py +238 -0
- upsetplot/tests/test_examples.py +19 -0
- upsetplot/tests/test_reformat.py +47 -0
- upsetplot/tests/test_upsetplot.py +1234 -0
- upsetplot/util.py +70 -0
- upsetplot_bombcell-0.10.0.dist-info/LICENSE +30 -0
- upsetplot_bombcell-0.10.0.dist-info/METADATA +220 -0
- upsetplot_bombcell-0.10.0.dist-info/RECORD +27 -0
- upsetplot_bombcell-0.10.0.dist-info/WHEEL +5 -0
- upsetplot_bombcell-0.10.0.dist-info/top_level.txt +3 -0
examples/plot_sizing.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
================================================
|
|
3
|
+
Design: Customizing element size and figure size
|
|
4
|
+
================================================
|
|
5
|
+
|
|
6
|
+
This example illustrates controlling sizing within an UpSet plot.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from matplotlib import pyplot as plt
|
|
10
|
+
|
|
11
|
+
from upsetplot import generate_counts, plot
|
|
12
|
+
|
|
13
|
+
example = generate_counts()
|
|
14
|
+
print(example)
|
|
15
|
+
|
|
16
|
+
plot(example)
|
|
17
|
+
plt.suptitle("Defaults")
|
|
18
|
+
plt.show()
|
|
19
|
+
|
|
20
|
+
##########################################################################
|
|
21
|
+
# upsetplot uses a grid of square "elements" to display. Controlling the
|
|
22
|
+
# size of these elements affects all components of the plot.
|
|
23
|
+
|
|
24
|
+
plot(example, element_size=40)
|
|
25
|
+
plt.suptitle("Increased element_size")
|
|
26
|
+
plt.show()
|
|
27
|
+
|
|
28
|
+
##########################################################################
|
|
29
|
+
# When setting ``figsize`` explicitly, you then need to pass the figure to
|
|
30
|
+
# ``plot``, and use ``element_size=None`` for optimal sizing.
|
|
31
|
+
|
|
32
|
+
fig = plt.figure(figsize=(10, 3))
|
|
33
|
+
plot(example, fig=fig, element_size=None)
|
|
34
|
+
plt.suptitle("Setting figsize explicitly")
|
|
35
|
+
plt.show()
|
|
36
|
+
|
|
37
|
+
##########################################################################
|
|
38
|
+
# Components in the plot can be resized by indicating how many elements
|
|
39
|
+
# they should equate to.
|
|
40
|
+
|
|
41
|
+
plot(example, intersection_plot_elements=3)
|
|
42
|
+
plt.suptitle("Decreased intersection_plot_elements")
|
|
43
|
+
plt.show()
|
|
44
|
+
|
|
45
|
+
##########################################################################
|
|
46
|
+
|
|
47
|
+
plot(example, totals_plot_elements=5)
|
|
48
|
+
plt.suptitle("Increased totals_plot_elements")
|
|
49
|
+
plt.show()
|
examples/plot_theming.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
============================
|
|
3
|
+
Design: Changing Plot Colors
|
|
4
|
+
============================
|
|
5
|
+
|
|
6
|
+
This example illustrates use of matplotlib and upsetplot color settings, aside
|
|
7
|
+
from matplotlib style sheets, which can control colors as well as grid lines,
|
|
8
|
+
fonts and tick display.
|
|
9
|
+
|
|
10
|
+
Upsetplot provides some color settings:
|
|
11
|
+
|
|
12
|
+
* ``facecolor``: sets the color for intersection size bars, and for active
|
|
13
|
+
matrix dots. Defaults to white on a dark background, otherwise black.
|
|
14
|
+
* ``other_dots_color``: sets the color for other (inactive) dots. Specify as a
|
|
15
|
+
color, or a float specifying opacity relative to facecolor.
|
|
16
|
+
* ``shading_color``: sets the color odd rows. Specify as a color, or a float
|
|
17
|
+
specifying opacity relative to facecolor.
|
|
18
|
+
|
|
19
|
+
For an introduction to matplotlib theming see:
|
|
20
|
+
|
|
21
|
+
* `Tutorial
|
|
22
|
+
<https://matplotlib.org/stable/tutorials/introductory/customizing.html>`__
|
|
23
|
+
* `Reference
|
|
24
|
+
<https://matplotlib.org/stable/gallery/style_sheets/style_sheets_reference.html>`__
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from matplotlib import pyplot as plt
|
|
28
|
+
|
|
29
|
+
from upsetplot import generate_counts, plot
|
|
30
|
+
|
|
31
|
+
example = generate_counts()
|
|
32
|
+
|
|
33
|
+
plot(example, facecolor="darkblue")
|
|
34
|
+
plt.suptitle('facecolor="darkblue"')
|
|
35
|
+
plt.show()
|
|
36
|
+
|
|
37
|
+
##########################################################################
|
|
38
|
+
|
|
39
|
+
plot(example, facecolor="darkblue", shading_color="lightgray")
|
|
40
|
+
plt.suptitle('facecolor="darkblue", shading_color="lightgray"')
|
|
41
|
+
plt.show()
|
|
42
|
+
|
|
43
|
+
##########################################################################
|
|
44
|
+
|
|
45
|
+
with plt.style.context("Solarize_Light2"):
|
|
46
|
+
plot(example)
|
|
47
|
+
plt.suptitle("matplotlib classic stylesheet")
|
|
48
|
+
plt.show()
|
|
49
|
+
|
|
50
|
+
##########################################################################
|
|
51
|
+
|
|
52
|
+
with plt.style.context("dark_background"):
|
|
53
|
+
plot(example, show_counts=True)
|
|
54
|
+
plt.suptitle("matplotlib dark_background stylesheet")
|
|
55
|
+
plt.show()
|
|
56
|
+
|
|
57
|
+
##########################################################################
|
|
58
|
+
|
|
59
|
+
with plt.style.context("dark_background"):
|
|
60
|
+
plot(example, show_counts=True, shading_color=0.15)
|
|
61
|
+
plt.suptitle("matplotlib dark_background stylesheet, shading_color=.15")
|
|
62
|
+
plt.show()
|
|
63
|
+
|
|
64
|
+
##########################################################################
|
|
65
|
+
|
|
66
|
+
with plt.style.context("dark_background"):
|
|
67
|
+
plot(example, show_counts=True, facecolor="red")
|
|
68
|
+
plt.suptitle('matplotlib dark_background, facecolor="red"')
|
|
69
|
+
plt.show()
|
|
70
|
+
|
|
71
|
+
##########################################################################
|
|
72
|
+
|
|
73
|
+
with plt.style.context("dark_background"):
|
|
74
|
+
plot(
|
|
75
|
+
example,
|
|
76
|
+
show_counts=True,
|
|
77
|
+
facecolor="red",
|
|
78
|
+
other_dots_color=0.4,
|
|
79
|
+
shading_color=0.2,
|
|
80
|
+
)
|
|
81
|
+
plt.suptitle("dark_background, red face, stronger other colors")
|
|
82
|
+
plt.show()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
===========================
|
|
3
|
+
Basic: Vertical orientation
|
|
4
|
+
===========================
|
|
5
|
+
|
|
6
|
+
This illustrates the effect of orientation='vertical'.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from matplotlib import pyplot as plt
|
|
10
|
+
|
|
11
|
+
from upsetplot import generate_counts, plot
|
|
12
|
+
|
|
13
|
+
example = generate_counts()
|
|
14
|
+
plot(example, orientation="vertical")
|
|
15
|
+
plt.suptitle("A vertical plot")
|
|
16
|
+
plt.show()
|
|
17
|
+
|
|
18
|
+
##########################################################################
|
|
19
|
+
|
|
20
|
+
plot(example, orientation="vertical", show_counts="{:d}")
|
|
21
|
+
plt.suptitle("A vertical plot with counts shown")
|
|
22
|
+
plt.show()
|
|
23
|
+
|
|
24
|
+
##########################################################################
|
|
25
|
+
|
|
26
|
+
plot(example, orientation="vertical", show_counts="{:d}", show_percentages=True)
|
|
27
|
+
plt.suptitle("With counts and percentages shown")
|
|
28
|
+
plt.show()
|
upsetplot/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__version__ = "0.10.0"
|
|
2
|
+
|
|
3
|
+
from .data import (
|
|
4
|
+
from_contents,
|
|
5
|
+
from_indicators,
|
|
6
|
+
from_memberships,
|
|
7
|
+
generate_counts,
|
|
8
|
+
generate_data,
|
|
9
|
+
generate_samples,
|
|
10
|
+
)
|
|
11
|
+
from .plotting import UpSet, plot
|
|
12
|
+
from .reformat import query
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"UpSet",
|
|
16
|
+
"generate_data",
|
|
17
|
+
"generate_counts",
|
|
18
|
+
"generate_samples",
|
|
19
|
+
"plot",
|
|
20
|
+
"from_memberships",
|
|
21
|
+
"from_contents",
|
|
22
|
+
"from_indicators",
|
|
23
|
+
"query",
|
|
24
|
+
]
|
upsetplot/data.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from numbers import Number
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_samples(seed=0, n_samples=10000, n_categories=3):
|
|
9
|
+
"""Generate artificial samples assigned to set intersections
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
seed : int
|
|
14
|
+
A seed for randomisation
|
|
15
|
+
n_samples : int
|
|
16
|
+
Number of samples to generate
|
|
17
|
+
n_categories : int
|
|
18
|
+
Number of categories (named "cat0", "cat1", ...) to generate
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
DataFrame
|
|
23
|
+
Field 'value' is a weight or score for each element.
|
|
24
|
+
Field 'index' is a unique id for each element.
|
|
25
|
+
Index includes a boolean indicator mask for each category.
|
|
26
|
+
|
|
27
|
+
Note: Further fields may be added in future versions.
|
|
28
|
+
|
|
29
|
+
See Also
|
|
30
|
+
--------
|
|
31
|
+
generate_counts : Generates the counts for each subset of categories
|
|
32
|
+
corresponding to these samples.
|
|
33
|
+
"""
|
|
34
|
+
rng = np.random.RandomState(seed)
|
|
35
|
+
df = pd.DataFrame({"value": np.zeros(n_samples)})
|
|
36
|
+
for i in range(n_categories):
|
|
37
|
+
r = rng.rand(n_samples)
|
|
38
|
+
df["cat%d" % i] = r > rng.rand()
|
|
39
|
+
df["value"] += r
|
|
40
|
+
|
|
41
|
+
df.reset_index(inplace=True)
|
|
42
|
+
df.set_index(["cat%d" % i for i in range(n_categories)], inplace=True)
|
|
43
|
+
return df
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_counts(seed=0, n_samples=10000, n_categories=3):
|
|
47
|
+
"""Generate artificial counts corresponding to set intersections
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
seed : int
|
|
52
|
+
A seed for randomisation
|
|
53
|
+
n_samples : int
|
|
54
|
+
Number of samples to generate statistics over
|
|
55
|
+
n_categories : int
|
|
56
|
+
Number of categories (named "cat0", "cat1", ...) to generate
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
Series
|
|
61
|
+
Counts indexed by boolean indicator mask for each category.
|
|
62
|
+
|
|
63
|
+
See Also
|
|
64
|
+
--------
|
|
65
|
+
generate_samples : Generates a DataFrame of samples that these counts are
|
|
66
|
+
derived from.
|
|
67
|
+
"""
|
|
68
|
+
df = generate_samples(seed=seed, n_samples=n_samples, n_categories=n_categories)
|
|
69
|
+
return df.value.groupby(level=list(range(n_categories))).count()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def generate_data(seed=0, n_samples=10000, n_sets=3, aggregated=False):
|
|
73
|
+
warnings.warn(
|
|
74
|
+
"generate_data was replaced by generate_counts in version "
|
|
75
|
+
"0.3 and will be removed in version 0.4.",
|
|
76
|
+
DeprecationWarning,
|
|
77
|
+
stacklevel=2,
|
|
78
|
+
)
|
|
79
|
+
if aggregated:
|
|
80
|
+
return generate_counts(seed=seed, n_samples=n_samples, n_categories=n_sets)
|
|
81
|
+
else:
|
|
82
|
+
return generate_samples(seed=seed, n_samples=n_samples, n_categories=n_sets)[
|
|
83
|
+
"value"
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def from_indicators(indicators, data=None):
|
|
88
|
+
"""Load category membership indicated by a boolean indicator matrix
|
|
89
|
+
|
|
90
|
+
This loader also supports the case where the indicator columns can be
|
|
91
|
+
derived from `data`.
|
|
92
|
+
|
|
93
|
+
.. versionadded:: 0.6
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
indicators : DataFrame-like of booleans, Sequence of str, or callable
|
|
98
|
+
Specifies the category indicators (boolean mask arrays) within
|
|
99
|
+
``data``, i.e. which records in ``data`` belong to which categories.
|
|
100
|
+
|
|
101
|
+
If a list of strings, these should be column names found in ``data``
|
|
102
|
+
whose values are boolean mask arrays.
|
|
103
|
+
|
|
104
|
+
If a DataFrame, its columns should correspond to categories, and its
|
|
105
|
+
index should be a subset of those in ``data``, values should be True
|
|
106
|
+
where a data record is in that category, and False or NA otherwise.
|
|
107
|
+
|
|
108
|
+
If callable, it will be applied to ``data`` after the latter is
|
|
109
|
+
converted to a Series or DataFrame.
|
|
110
|
+
|
|
111
|
+
data : Series-like or DataFrame-like, optional
|
|
112
|
+
If given, the index of category membership is attached to this data.
|
|
113
|
+
It must have the same length as `indicators`.
|
|
114
|
+
If not given, the series will contain the value 1.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
DataFrame or Series
|
|
119
|
+
`data` is returned with its index indicating category membership.
|
|
120
|
+
It will be a Series if `data` is a Series or 1d numeric array or None.
|
|
121
|
+
|
|
122
|
+
Notes
|
|
123
|
+
-----
|
|
124
|
+
Categories with indicators that are all False will be removed.
|
|
125
|
+
|
|
126
|
+
Examples
|
|
127
|
+
--------
|
|
128
|
+
>>> import pandas as pd
|
|
129
|
+
>>> from upsetplot import from_indicators
|
|
130
|
+
>>>
|
|
131
|
+
>>> # Just indicators:
|
|
132
|
+
>>> indicators = {"cat1": [True, False, True, False],
|
|
133
|
+
... "cat2": [False, True, False, False],
|
|
134
|
+
... "cat3": [True, True, False, False]}
|
|
135
|
+
>>> from_indicators(indicators)
|
|
136
|
+
cat1 cat2 cat3
|
|
137
|
+
True False True 1.0
|
|
138
|
+
False True True 1.0
|
|
139
|
+
True False False 1.0
|
|
140
|
+
False False False 1.0
|
|
141
|
+
Name: ones, dtype: float64
|
|
142
|
+
>>>
|
|
143
|
+
>>> # Where indicators are included within data, specifying
|
|
144
|
+
>>> # columns by name:
|
|
145
|
+
>>> data = pd.DataFrame({"value": [5, 4, 6, 4], **indicators})
|
|
146
|
+
>>> from_indicators(["cat1", "cat3"], data=data)
|
|
147
|
+
value cat1 cat2 cat3
|
|
148
|
+
cat1 cat3
|
|
149
|
+
True True 5 True False True
|
|
150
|
+
False True 4 False True True
|
|
151
|
+
True False 6 True False False
|
|
152
|
+
False False 4 False False False
|
|
153
|
+
>>>
|
|
154
|
+
>>> # Making indicators out of all boolean columns:
|
|
155
|
+
>>> from_indicators(lambda data: data.select_dtypes(bool), data=data)
|
|
156
|
+
value cat1 cat2 cat3
|
|
157
|
+
cat1 cat2 cat3
|
|
158
|
+
True False True 5 True False True
|
|
159
|
+
False True True 4 False True True
|
|
160
|
+
True False False 6 True False False
|
|
161
|
+
False False False 4 False False False
|
|
162
|
+
>>>
|
|
163
|
+
>>> # Using a dataset with missing data, we can use missingness as
|
|
164
|
+
>>> # an indicator:
|
|
165
|
+
>>> data = pd.DataFrame({"val1": [pd.NA, .7, pd.NA, .9],
|
|
166
|
+
... "val2": ["male", pd.NA, "female", "female"],
|
|
167
|
+
... "val3": [pd.NA, pd.NA, 23000, 78000]})
|
|
168
|
+
>>> from_indicators(pd.isna, data=data)
|
|
169
|
+
val1 val2 val3
|
|
170
|
+
val1 val2 val3
|
|
171
|
+
True False True <NA> male <NA>
|
|
172
|
+
False True True 0.7 <NA> <NA>
|
|
173
|
+
True False False <NA> female 23000
|
|
174
|
+
False False False 0.9 female 78000
|
|
175
|
+
"""
|
|
176
|
+
if data is not None:
|
|
177
|
+
data = _convert_to_pandas(data)
|
|
178
|
+
|
|
179
|
+
if callable(indicators):
|
|
180
|
+
if data is None:
|
|
181
|
+
raise ValueError("data must be provided when indicators is " "callable")
|
|
182
|
+
indicators = indicators(data)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
indicators[0]
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
else:
|
|
189
|
+
if isinstance(indicators[0], (str, int)):
|
|
190
|
+
if data is None:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"data must be provided when indicators are "
|
|
193
|
+
"specified as a list of columns"
|
|
194
|
+
)
|
|
195
|
+
if isinstance(indicators, tuple):
|
|
196
|
+
raise ValueError("indicators as tuple is not supported")
|
|
197
|
+
# column array
|
|
198
|
+
indicators = data[indicators]
|
|
199
|
+
|
|
200
|
+
indicators = pd.DataFrame(indicators)
|
|
201
|
+
# Fill NaN with False, using where() to avoid pandas FutureWarning about downcasting
|
|
202
|
+
indicators = indicators.where(pd.notna(indicators), False).infer_objects(copy=False)
|
|
203
|
+
# drop all-False (should we be dropping all-True also? making an option?)
|
|
204
|
+
indicators = indicators.loc[:, indicators.any(axis=0)]
|
|
205
|
+
|
|
206
|
+
if not all(dtype.kind == "b" for dtype in indicators.dtypes):
|
|
207
|
+
raise ValueError("The indicators must all be boolean")
|
|
208
|
+
|
|
209
|
+
if data is not None:
|
|
210
|
+
if not (
|
|
211
|
+
isinstance(indicators.index, pd.RangeIndex)
|
|
212
|
+
and indicators.index[0] == 0
|
|
213
|
+
and indicators.index[-1] == len(data) - 1
|
|
214
|
+
):
|
|
215
|
+
# index is specified on indicators. Need to align it to data
|
|
216
|
+
if not indicators.index.isin(data.index).all():
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"If indicators.index is not the default, "
|
|
219
|
+
"all its values must be present in "
|
|
220
|
+
"data.index"
|
|
221
|
+
)
|
|
222
|
+
indicators = indicators.reindex(index=data.index, fill_value=False)
|
|
223
|
+
else:
|
|
224
|
+
data = pd.Series(np.ones(len(indicators)), name="ones")
|
|
225
|
+
|
|
226
|
+
indicators.set_index(list(indicators.columns), inplace=True)
|
|
227
|
+
# Ensure we always have a MultiIndex, even with a single category
|
|
228
|
+
if not isinstance(indicators.index, pd.MultiIndex):
|
|
229
|
+
indicators.index = pd.MultiIndex.from_arrays(
|
|
230
|
+
[indicators.index], names=[indicators.index.name]
|
|
231
|
+
)
|
|
232
|
+
data.index = indicators.index
|
|
233
|
+
|
|
234
|
+
return data
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _convert_to_pandas(data, copy=True):
|
|
238
|
+
is_series = False
|
|
239
|
+
if hasattr(data, "loc"):
|
|
240
|
+
if copy:
|
|
241
|
+
data = data.copy(deep=False)
|
|
242
|
+
is_series = data.ndim == 1
|
|
243
|
+
elif len(data):
|
|
244
|
+
try:
|
|
245
|
+
is_series = isinstance(data[0], Number)
|
|
246
|
+
except KeyError:
|
|
247
|
+
is_series = False
|
|
248
|
+
return pd.Series(data) if is_series else pd.DataFrame(data)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def from_memberships(memberships, data=None):
|
|
252
|
+
"""Load data where each sample has a collection of category names
|
|
253
|
+
|
|
254
|
+
The output should be suitable for passing to `UpSet` or `plot`.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
memberships : sequence of collections of strings
|
|
259
|
+
Each element corresponds to a data point, indicating the sets it is a
|
|
260
|
+
member of. Each category is named by a string.
|
|
261
|
+
data : Series-like or DataFrame-like, optional
|
|
262
|
+
If given, the index of category memberships is attached to this data.
|
|
263
|
+
It must have the same length as `memberships`.
|
|
264
|
+
If not given, the series will contain the value 1.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
DataFrame or Series
|
|
269
|
+
`data` is returned with its index indicating category membership.
|
|
270
|
+
It will be a Series if `data` is a Series or 1d numeric array.
|
|
271
|
+
The index will have levels ordered by category names.
|
|
272
|
+
|
|
273
|
+
Examples
|
|
274
|
+
--------
|
|
275
|
+
>>> from upsetplot import from_memberships
|
|
276
|
+
>>> from_memberships([
|
|
277
|
+
... ['cat1', 'cat3'],
|
|
278
|
+
... ['cat2', 'cat3'],
|
|
279
|
+
... ['cat1'],
|
|
280
|
+
... []
|
|
281
|
+
... ])
|
|
282
|
+
cat1 cat2 cat3
|
|
283
|
+
True False True 1
|
|
284
|
+
False True True 1
|
|
285
|
+
True False False 1
|
|
286
|
+
False False False 1
|
|
287
|
+
Name: ones, dtype: ...
|
|
288
|
+
>>> # now with data:
|
|
289
|
+
>>> import numpy as np
|
|
290
|
+
>>> from_memberships([
|
|
291
|
+
... ['cat1', 'cat3'],
|
|
292
|
+
... ['cat2', 'cat3'],
|
|
293
|
+
... ['cat1'],
|
|
294
|
+
... []
|
|
295
|
+
... ], data=np.arange(12).reshape(4, 3))
|
|
296
|
+
0 1 2
|
|
297
|
+
cat1 cat2 cat3
|
|
298
|
+
True False True 0 1 2
|
|
299
|
+
False True True 3 4 5
|
|
300
|
+
True False False 6 7 8
|
|
301
|
+
False False False 9 10 11
|
|
302
|
+
"""
|
|
303
|
+
df = pd.DataFrame([{name: True for name in names} for names in memberships])
|
|
304
|
+
for set_name in df.columns:
|
|
305
|
+
if not hasattr(set_name, "lower"):
|
|
306
|
+
raise ValueError("Category names should be strings")
|
|
307
|
+
if df.shape[1] == 0:
|
|
308
|
+
raise ValueError("Require at least one category. None were found.")
|
|
309
|
+
df.sort_index(axis=1, inplace=True)
|
|
310
|
+
# Convert to bool, treating NaN as False (avoids pandas FutureWarning)
|
|
311
|
+
df = df.eq(True)
|
|
312
|
+
df.set_index(list(df.columns), inplace=True)
|
|
313
|
+
# Ensure we always have a MultiIndex, even with a single category
|
|
314
|
+
if not isinstance(df.index, pd.MultiIndex):
|
|
315
|
+
df.index = pd.MultiIndex.from_arrays([df.index], names=[df.index.name])
|
|
316
|
+
if data is None:
|
|
317
|
+
return df.assign(ones=1)["ones"]
|
|
318
|
+
|
|
319
|
+
data = _convert_to_pandas(data)
|
|
320
|
+
if len(data) != len(df):
|
|
321
|
+
raise ValueError(
|
|
322
|
+
"memberships and data must have the same length. "
|
|
323
|
+
"Got len(memberships) == %d, len(data) == %d"
|
|
324
|
+
% (len(memberships), len(data))
|
|
325
|
+
)
|
|
326
|
+
data.index = df.index
|
|
327
|
+
return data
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def from_contents(contents, data=None, id_column="id"):
|
|
331
|
+
"""Build data from category listings
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
contents : Mapping (or iterable over pairs) of strings to sets
|
|
336
|
+
Keys are category names, values are sets of identifiers (int or
|
|
337
|
+
string).
|
|
338
|
+
data : DataFrame, optional
|
|
339
|
+
If provided, this should be indexed by the identifiers used in
|
|
340
|
+
`contents`.
|
|
341
|
+
id_column : str, default='id'
|
|
342
|
+
The column name to use for the identifiers in the output.
|
|
343
|
+
|
|
344
|
+
Returns
|
|
345
|
+
-------
|
|
346
|
+
DataFrame
|
|
347
|
+
`data` is returned with its index indicating category membership,
|
|
348
|
+
including a column named according to id_column.
|
|
349
|
+
If data is not given, the order of rows is not assured.
|
|
350
|
+
|
|
351
|
+
Notes
|
|
352
|
+
-----
|
|
353
|
+
The order of categories in the output DataFrame is determined from
|
|
354
|
+
`contents`, which may have non-deterministic iteration order.
|
|
355
|
+
|
|
356
|
+
Examples
|
|
357
|
+
--------
|
|
358
|
+
>>> from upsetplot import from_contents
|
|
359
|
+
>>> contents = {'cat1': ['a', 'b', 'c'],
|
|
360
|
+
... 'cat2': ['b', 'd'],
|
|
361
|
+
... 'cat3': ['e']}
|
|
362
|
+
>>> from_contents(contents)
|
|
363
|
+
id
|
|
364
|
+
cat1 cat2 cat3
|
|
365
|
+
True False False a
|
|
366
|
+
True False b
|
|
367
|
+
False False c
|
|
368
|
+
False True False d
|
|
369
|
+
False True e
|
|
370
|
+
>>> import pandas as pd
|
|
371
|
+
>>> contents = {'cat1': [0, 1, 2],
|
|
372
|
+
... 'cat2': [1, 3],
|
|
373
|
+
... 'cat3': [4]}
|
|
374
|
+
>>> data = pd.DataFrame({'favourite': ['green', 'red', 'red',
|
|
375
|
+
... 'yellow', 'blue']})
|
|
376
|
+
>>> from_contents(contents, data=data)
|
|
377
|
+
id favourite
|
|
378
|
+
cat1 cat2 cat3
|
|
379
|
+
True False False 0 green
|
|
380
|
+
True False 1 red
|
|
381
|
+
False False 2 red
|
|
382
|
+
False True False 3 yellow
|
|
383
|
+
False True 4 blue
|
|
384
|
+
"""
|
|
385
|
+
cat_series = [
|
|
386
|
+
pd.Series(True, index=list(elements), name=name)
|
|
387
|
+
for name, elements in contents.items()
|
|
388
|
+
]
|
|
389
|
+
if not all(s.index.is_unique for s in cat_series):
|
|
390
|
+
raise ValueError("Got duplicate ids in a category")
|
|
391
|
+
|
|
392
|
+
df = pd.concat(cat_series, axis=1, sort=False)
|
|
393
|
+
if id_column in df.columns:
|
|
394
|
+
raise ValueError("A category cannot be named %r" % id_column)
|
|
395
|
+
# Convert to bool, treating NaN as False (avoids pandas FutureWarning)
|
|
396
|
+
df = df.eq(True)
|
|
397
|
+
cat_names = list(df.columns)
|
|
398
|
+
|
|
399
|
+
if data is not None:
|
|
400
|
+
if set(df.columns).intersection(data.columns):
|
|
401
|
+
raise ValueError("Data columns overlap with category names")
|
|
402
|
+
if id_column in data.columns:
|
|
403
|
+
raise ValueError("data cannot contain a column named %r" % id_column)
|
|
404
|
+
not_in_data = df.drop(data.index, axis=0, errors="ignore")
|
|
405
|
+
if len(not_in_data):
|
|
406
|
+
raise ValueError(
|
|
407
|
+
"Found identifiers in contents that are not in "
|
|
408
|
+
"data: %r" % not_in_data.index.values
|
|
409
|
+
)
|
|
410
|
+
# Use eq(True) to convert NaN to False without FutureWarning
|
|
411
|
+
df = df.reindex(index=data.index).eq(True)
|
|
412
|
+
df = pd.concat([data, df], axis=1, sort=False)
|
|
413
|
+
df.index.name = id_column
|
|
414
|
+
result = df.reset_index().set_index(cat_names)
|
|
415
|
+
# Ensure we always have a MultiIndex, even with a single category
|
|
416
|
+
if not isinstance(result.index, pd.MultiIndex):
|
|
417
|
+
result.index = pd.MultiIndex.from_arrays(
|
|
418
|
+
[result.index], names=[result.index.name]
|
|
419
|
+
)
|
|
420
|
+
return result
|