theseusplot 0.1.0__tar.gz → 0.1.1__tar.gz

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.
Files changed (36) hide show
  1. {theseusplot-0.1.0 → theseusplot-0.1.1}/.github/workflows/publish.yml +3 -0
  2. {theseusplot-0.1.0 → theseusplot-0.1.1}/PKG-INFO +21 -13
  3. theseusplot-0.1.1/README-figures/factor_column-13.png +0 -0
  4. theseusplot-0.1.1/README-figures/overview-1.png +0 -0
  5. theseusplot-0.1.1/README-figures/plot_carrier-5.png +0 -0
  6. theseusplot-0.1.1/README-figures/plot_carrier_n-7.png +0 -0
  7. theseusplot-0.1.1/README-figures/plot_dep_delay-9.png +0 -0
  8. theseusplot-0.1.1/README-figures/plot_dep_delay_n-11.png +0 -0
  9. theseusplot-0.1.1/README-figures/plot_origin-3.png +0 -0
  10. {theseusplot-0.1.0 → theseusplot-0.1.1}/README.Rmd +19 -4
  11. {theseusplot-0.1.0 → theseusplot-0.1.1}/README.md +20 -12
  12. {theseusplot-0.1.0 → theseusplot-0.1.1}/pyproject.toml +1 -1
  13. theseusplot-0.1.1/scripts/prepare_pypi_readme.py +21 -0
  14. {theseusplot-0.1.0 → theseusplot-0.1.1}/src/theseusplot/_ship.py +12 -9
  15. theseusplot-0.1.1/tests/test_plot.py +255 -0
  16. theseusplot-0.1.0/README-figures/factor_column-13.png +0 -0
  17. theseusplot-0.1.0/README-figures/overview-1.png +0 -0
  18. theseusplot-0.1.0/README-figures/plot_carrier-5.png +0 -0
  19. theseusplot-0.1.0/README-figures/plot_carrier_n-7.png +0 -0
  20. theseusplot-0.1.0/README-figures/plot_dep_delay-9.png +0 -0
  21. theseusplot-0.1.0/README-figures/plot_dep_delay_n-11.png +0 -0
  22. theseusplot-0.1.0/README-figures/plot_origin-3.png +0 -0
  23. theseusplot-0.1.0/tests/test_plot.py +0 -118
  24. {theseusplot-0.1.0 → theseusplot-0.1.1}/.github/workflows/ci.yml +0 -0
  25. {theseusplot-0.1.0 → theseusplot-0.1.1}/TheseusPlot_py.Rproj +0 -0
  26. {theseusplot-0.1.0 → theseusplot-0.1.1}/src/theseusplot/__init__.py +0 -0
  27. {theseusplot-0.1.0 → theseusplot-0.1.1}/src/theseusplot/_config.py +0 -0
  28. {theseusplot-0.1.0 → theseusplot-0.1.1}/src/theseusplot/py.typed +0 -0
  29. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/fixtures/table_factor_order.json +0 -0
  30. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/fixtures/table_missing_asymmetric_categories.json +0 -0
  31. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/fixtures/table_top_n_aggregation.json +0 -0
  32. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/test_continuous_binning.py +0 -0
  33. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/test_contribution_algorithm.py +0 -0
  34. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/test_fixtures.py +0 -0
  35. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/test_public_api.py +0 -0
  36. {theseusplot-0.1.0 → theseusplot-0.1.1}/tests/test_table_behavior.py +0 -0
@@ -24,6 +24,9 @@ jobs:
24
24
  - name: Install build dependencies
25
25
  run: python -m pip install --upgrade build twine
26
26
 
27
+ - name: Prepare README for PyPI
28
+ run: python scripts/prepare_pypi_readme.py
29
+
27
30
  - name: Build source and wheel distributions
28
31
  run: python -m build
29
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: theseusplot
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Python port of TheseusPlot for decomposing differences in rate metrics.
5
5
  Author: TheseusPlot.py contributors
6
6
  License: MIT
@@ -23,8 +23,9 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  <!-- badges: start -->
25
25
 
26
+ [![PyPI version](https://img.shields.io/pypi/v/theseusplot.svg)](https://pypi.org/project/theseusplot/)
27
+ [![Downloads](https://static.pepy.tech/badge/theseusplot)](https://pepy.tech/project/theseusplot)
26
28
  [![CI](https://github.com/hoxo-m/TheseusPlot_py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoxo-m/TheseusPlot_py/actions/workflows/ci.yml)
27
-
28
29
  <!-- badges: end -->
29
30
 
30
31
  ## 1. Overview
@@ -58,7 +59,7 @@ Thus, the contribution of the female group is -0.2 percentage points.
58
59
 
59
60
  When visualized, the results appear as follows:
60
61
 
61
- <img src="README-figures/overview-1.png" alt="" width="500" />
62
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/overview-1.png" alt="" width="500" />
62
63
 
63
64
  From this plot, we can see that the decline in the metric is primarily
64
65
  driven by the male group. We call this visualization the “Theseus Plot.”
@@ -68,18 +69,25 @@ Theseus Plots for various attributes.
68
69
 
69
70
  ## 2. Installation
70
71
 
71
- You can install the development version from
72
- [GitHub](https://github.com/hoxo-m/TheseusPlot_py) with:
72
+ You can install the **theseusplot** package from
73
+ [PyPI](https://pypi.org/project/theseusplot/) with:
73
74
 
74
75
  ``` bash
75
- python -m pip install "git+https://github.com/hoxo-m/TheseusPlot_py.git"
76
+ python -m pip install theseusplot
76
77
  ```
77
78
 
78
79
  You can install the optional dependencies for examples and documentation
79
80
  data with:
80
81
 
81
82
  ``` bash
82
- python -m pip install "theseusplot[examples] @ git+https://github.com/hoxo-m/TheseusPlot_py.git"
83
+ python -m pip install "theseusplot[examples]"
84
+ ```
85
+
86
+ You can install the development version from
87
+ [GitHub](https://github.com/hoxo-m/TheseusPlot_py) with:
88
+
89
+ ``` bash
90
+ python -m pip install "git+https://github.com/hoxo-m/TheseusPlot_py.git"
83
91
  ```
84
92
 
85
93
  ## 3. Details
@@ -168,7 +176,7 @@ fig, ax = ship.plot("origin")
168
176
  fig.show()
169
177
  ```
170
178
 
171
- <img src="README-figures/plot_origin-3.png" alt="" width="500" />
179
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_origin-3.png" alt="" width="500" />
172
180
 
173
181
  New York City has three major airports, and Newark Liberty International
174
182
  Airport (EWR) accounted for the largest share of the decline in the
@@ -209,7 +217,7 @@ fig, ax = ship.plot_flip("carrier")
209
217
  fig.show()
210
218
  ```
211
219
 
212
- <img src="README-figures/plot_carrier-5.png" alt="" width="500" />
220
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_carrier-5.png" alt="" width="500" />
213
221
 
214
222
  When the number of subgroups is large, those with small contributions
215
223
  are automatically grouped together. By default, this happens when there
@@ -221,7 +229,7 @@ fig, ax = ship.plot_flip("carrier", n=6)
221
229
  fig.show()
222
230
  ```
223
231
 
224
- <img src="README-figures/plot_carrier_n-7.png" alt="" width="500" />
232
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_carrier_n-7.png" alt="" width="500" />
225
233
 
226
234
  From this plot, JetBlue Airways and United Air Lines appear to have the
227
235
  largest contributions to the decline in on-time arrival rate.
@@ -237,7 +245,7 @@ fig, ax = ship.plot_flip("dep_delay")
237
245
  fig.show()
238
246
  ```
239
247
 
240
- <img src="README-figures/plot_dep_delay-9.png" alt="" width="500" />
248
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_dep_delay-9.png" alt="" width="500" />
241
249
 
242
250
  By default, continuous variables are discretized so that each subgroup
243
251
  has roughly equal sample sizes, with the number of bins set to 10. You
@@ -251,7 +259,7 @@ fig, ax = ship.plot_flip("dep_delay", continuous=continuous_config(n=3))
251
259
  fig.show()
252
260
  ```
253
261
 
254
- <img src="README-figures/plot_dep_delay_n-11.png" alt="" width="500" />
262
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_dep_delay_n-11.png" alt="" width="500" />
255
263
 
256
264
  This result shows that both a decrease in on-time departures and an
257
265
  increase in delayed departures contributed to the decline in on-time
@@ -305,7 +313,7 @@ fig, ax = ship.plot("segment")
305
313
  fig.show()
306
314
  ```
307
315
 
308
- <img src="README-figures/factor_column-13.png" alt="" width="500" />
316
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/factor_column-13.png" alt="" width="500" />
309
317
 
310
318
  Even if the contribution of `"High"` is larger than that of `"Low"` or
311
319
  `"Medium"`, the rows and bars are shown in the order
@@ -18,12 +18,20 @@ knitr::opts_chunk$set(
18
18
  )
19
19
 
20
20
  library(reticulate)
21
+ py_install(envname = "theseusplot-env", packages = ".", pip = TRUE)
21
22
  use_virtualenv("theseusplot-env", required = TRUE)
23
+
24
+ readme_base_url <- Sys.getenv("THESEUSPLOT_README_BASE_URL", unset = "")
25
+ if (nzchar(readme_base_url)) {
26
+ knitr::opts_knit$set(base.url = readme_base_url)
27
+ }
22
28
  ```
23
29
 
24
30
  # TheseusPlot: Visualizing Decomposition of Differences in Rate Metrics
25
31
 
26
32
  <!-- badges: start -->
33
+ [![PyPI version](https://img.shields.io/pypi/v/theseusplot.svg)](https://pypi.org/project/theseusplot/)
34
+ [![Downloads](https://static.pepy.tech/badge/theseusplot)](https://pepy.tech/project/theseusplot)
27
35
  [![CI](https://github.com/hoxo-m/TheseusPlot_py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoxo-m/TheseusPlot_py/actions/workflows/ci.yml)
28
36
  <!-- badges: end -->
29
37
 
@@ -87,18 +95,25 @@ The **TheseusPlot** package is designed to make it easy to generate Theseus Plot
87
95
 
88
96
  ## 2. Installation
89
97
 
90
- You can install the development version from
91
- [GitHub](https://github.com/hoxo-m/TheseusPlot_py) with:
98
+ You can install the **theseusplot** package from
99
+ [PyPI](https://pypi.org/project/theseusplot/) with:
92
100
 
93
101
  ```bash
94
- python -m pip install "git+https://github.com/hoxo-m/TheseusPlot_py.git"
102
+ python -m pip install theseusplot
95
103
  ```
96
104
 
97
105
  You can install the optional dependencies for examples and documentation data
98
106
  with:
99
107
 
100
108
  ```bash
101
- python -m pip install "theseusplot[examples] @ git+https://github.com/hoxo-m/TheseusPlot_py.git"
109
+ python -m pip install "theseusplot[examples]"
110
+ ```
111
+
112
+ You can install the development version from
113
+ [GitHub](https://github.com/hoxo-m/TheseusPlot_py) with:
114
+
115
+ ```bash
116
+ python -m pip install "git+https://github.com/hoxo-m/TheseusPlot_py.git"
102
117
  ```
103
118
 
104
119
  ## 3. Details
@@ -5,8 +5,9 @@
5
5
 
6
6
  <!-- badges: start -->
7
7
 
8
+ [![PyPI version](https://img.shields.io/pypi/v/theseusplot.svg)](https://pypi.org/project/theseusplot/)
9
+ [![Downloads](https://static.pepy.tech/badge/theseusplot)](https://pepy.tech/project/theseusplot)
8
10
  [![CI](https://github.com/hoxo-m/TheseusPlot_py/actions/workflows/ci.yml/badge.svg)](https://github.com/hoxo-m/TheseusPlot_py/actions/workflows/ci.yml)
9
-
10
11
  <!-- badges: end -->
11
12
 
12
13
  ## 1. Overview
@@ -40,7 +41,7 @@ Thus, the contribution of the female group is -0.2 percentage points.
40
41
 
41
42
  When visualized, the results appear as follows:
42
43
 
43
- <img src="README-figures/overview-1.png" alt="" width="500" />
44
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/overview-1.png" alt="" width="500" />
44
45
 
45
46
  From this plot, we can see that the decline in the metric is primarily
46
47
  driven by the male group. We call this visualization the “Theseus Plot.”
@@ -50,18 +51,25 @@ Theseus Plots for various attributes.
50
51
 
51
52
  ## 2. Installation
52
53
 
53
- You can install the development version from
54
- [GitHub](https://github.com/hoxo-m/TheseusPlot_py) with:
54
+ You can install the **theseusplot** package from
55
+ [PyPI](https://pypi.org/project/theseusplot/) with:
55
56
 
56
57
  ``` bash
57
- python -m pip install "git+https://github.com/hoxo-m/TheseusPlot_py.git"
58
+ python -m pip install theseusplot
58
59
  ```
59
60
 
60
61
  You can install the optional dependencies for examples and documentation
61
62
  data with:
62
63
 
63
64
  ``` bash
64
- python -m pip install "theseusplot[examples] @ git+https://github.com/hoxo-m/TheseusPlot_py.git"
65
+ python -m pip install "theseusplot[examples]"
66
+ ```
67
+
68
+ You can install the development version from
69
+ [GitHub](https://github.com/hoxo-m/TheseusPlot_py) with:
70
+
71
+ ``` bash
72
+ python -m pip install "git+https://github.com/hoxo-m/TheseusPlot_py.git"
65
73
  ```
66
74
 
67
75
  ## 3. Details
@@ -150,7 +158,7 @@ fig, ax = ship.plot("origin")
150
158
  fig.show()
151
159
  ```
152
160
 
153
- <img src="README-figures/plot_origin-3.png" alt="" width="500" />
161
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_origin-3.png" alt="" width="500" />
154
162
 
155
163
  New York City has three major airports, and Newark Liberty International
156
164
  Airport (EWR) accounted for the largest share of the decline in the
@@ -191,7 +199,7 @@ fig, ax = ship.plot_flip("carrier")
191
199
  fig.show()
192
200
  ```
193
201
 
194
- <img src="README-figures/plot_carrier-5.png" alt="" width="500" />
202
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_carrier-5.png" alt="" width="500" />
195
203
 
196
204
  When the number of subgroups is large, those with small contributions
197
205
  are automatically grouped together. By default, this happens when there
@@ -203,7 +211,7 @@ fig, ax = ship.plot_flip("carrier", n=6)
203
211
  fig.show()
204
212
  ```
205
213
 
206
- <img src="README-figures/plot_carrier_n-7.png" alt="" width="500" />
214
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_carrier_n-7.png" alt="" width="500" />
207
215
 
208
216
  From this plot, JetBlue Airways and United Air Lines appear to have the
209
217
  largest contributions to the decline in on-time arrival rate.
@@ -219,7 +227,7 @@ fig, ax = ship.plot_flip("dep_delay")
219
227
  fig.show()
220
228
  ```
221
229
 
222
- <img src="README-figures/plot_dep_delay-9.png" alt="" width="500" />
230
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_dep_delay-9.png" alt="" width="500" />
223
231
 
224
232
  By default, continuous variables are discretized so that each subgroup
225
233
  has roughly equal sample sizes, with the number of bins set to 10. You
@@ -233,7 +241,7 @@ fig, ax = ship.plot_flip("dep_delay", continuous=continuous_config(n=3))
233
241
  fig.show()
234
242
  ```
235
243
 
236
- <img src="README-figures/plot_dep_delay_n-11.png" alt="" width="500" />
244
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/plot_dep_delay_n-11.png" alt="" width="500" />
237
245
 
238
246
  This result shows that both a decrease in on-time departures and an
239
247
  increase in delayed departures contributed to the decline in on-time
@@ -287,7 +295,7 @@ fig, ax = ship.plot("segment")
287
295
  fig.show()
288
296
  ```
289
297
 
290
- <img src="README-figures/factor_column-13.png" alt="" width="500" />
298
+ <img src="https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/README-figures/factor_column-13.png" alt="" width="500" />
291
299
 
292
300
  Even if the contribution of `"High"` is larger than that of `"Low"` or
293
301
  `"Medium"`, the rows and bars are shown in the order
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "theseusplot"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Python port of TheseusPlot for decomposing differences in rate metrics."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,21 @@
1
+ """Prepare README image URLs for PyPI rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ RAW_BASE_URL = "https://raw.githubusercontent.com/hoxo-m/TheseusPlot_py/main/"
8
+ README_PATH = Path("README.md")
9
+
10
+
11
+ def main() -> None:
12
+ readme = README_PATH.read_text(encoding="utf-8")
13
+ readme = readme.replace(
14
+ 'src="README-figures/',
15
+ f'src="{RAW_BASE_URL}README-figures/',
16
+ )
17
+ README_PATH.write_text(readme, encoding="utf-8")
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()
@@ -20,8 +20,8 @@ _MIN_BREAK_COUNT = 2
20
20
  _REFITTED_COLOR = "#00BFC4"
21
21
  _ORIGINAL_SIZE_COLOR = "#7CAE00"
22
22
  _REFITTED_SIZE_COLOR = "#C77CFF"
23
- _POSITIVE_COLOR = "#F8766D"
24
- _NEGATIVE_COLOR = "#00BFC4"
23
+ _POSITIVE_COLOR = "#00BFC4"
24
+ _NEGATIVE_COLOR = "#F8766D"
25
25
 
26
26
 
27
27
  class ShipOfTheseus:
@@ -695,7 +695,6 @@ class ShipOfTheseus:
695
695
  ax.axvline(0, color="#333333", linewidth=0.8)
696
696
  ax.set_yticks(positions)
697
697
  ax.set_yticklabels(waterfall["items"].astype(str))
698
- ax.invert_yaxis()
699
698
  ax.set_xlabel(self.y_label or "")
700
699
  ax.set_ylabel("")
701
700
  ax.set_title(column)
@@ -733,7 +732,6 @@ class ShipOfTheseus:
733
732
  row["scaled_n"],
734
733
  width=width,
735
734
  color=colors[group],
736
- alpha=0.35,
737
735
  linewidth=0,
738
736
  zorder=1,
739
737
  )
@@ -760,7 +758,6 @@ class ShipOfTheseus:
760
758
  row["scaled_n"],
761
759
  height=height,
762
760
  color=colors[group],
763
- alpha=0.35,
764
761
  linewidth=0,
765
762
  zorder=1,
766
763
  )
@@ -774,7 +771,7 @@ class ShipOfTheseus:
774
771
  colors = [
775
772
  _REFITTED_COLOR
776
773
  if row["kind"] == "total"
777
- else (_POSITIVE_COLOR if row["amount"] >= 0 else _NEGATIVE_COLOR)
774
+ else self._contribution_color(float(row["amount"]))
778
775
  for _, row in waterfall.iterrows()
779
776
  ]
780
777
  ax.bar(
@@ -813,7 +810,7 @@ class ShipOfTheseus:
813
810
  colors = [
814
811
  _REFITTED_COLOR
815
812
  if row["kind"] == "total"
816
- else (_POSITIVE_COLOR if row["amount"] >= 0 else _NEGATIVE_COLOR)
813
+ else self._contribution_color(-float(row["amount"]))
817
814
  for _, row in waterfall.iterrows()
818
815
  ]
819
816
  ax.barh(
@@ -827,10 +824,12 @@ class ShipOfTheseus:
827
824
  zorder=3,
828
825
  )
829
826
  for position, (_, row) in zip(positions, waterfall.iterrows(), strict=True):
830
- value = self._format_plot_value(float(row["amount"]))
827
+ amount = float(row["amount"])
828
+ display_amount = -amount if row["kind"] == "contribution" else amount
829
+ value = self._format_plot_value(display_amount)
831
830
  x = float(row["bottom"]) + float(row["height"])
832
831
  ha = "left"
833
- if float(row["amount"]) < 0:
832
+ if amount < 0:
834
833
  x = float(row["bottom"])
835
834
  ha = "right"
836
835
  ax.text(
@@ -843,6 +842,10 @@ class ShipOfTheseus:
843
842
  zorder=4,
844
843
  )
845
844
 
845
+ @staticmethod
846
+ def _contribution_color(amount: float) -> str:
847
+ return _NEGATIVE_COLOR if amount < 0 else _POSITIVE_COLOR
848
+
846
849
  @staticmethod
847
850
  def _draw_connectors(
848
851
  ax: Any,
@@ -0,0 +1,255 @@
1
+ import pandas as pd
2
+ import pytest
3
+
4
+ from theseusplot import create_ship
5
+
6
+
7
+ def _require_pyplot():
8
+ matplotlib = pytest.importorskip("matplotlib")
9
+ matplotlib.use("Agg", force=True)
10
+ return pytest.importorskip("matplotlib.pyplot")
11
+
12
+
13
+ def _waterfall_colors(ax) -> list[str]:
14
+ colors = pytest.importorskip("matplotlib.colors")
15
+ return [
16
+ colors.to_hex(patch.get_facecolor())
17
+ for patch in ax.patches
18
+ if patch.get_zorder() == 3
19
+ ]
20
+
21
+
22
+ def _size_bar_styles(ax) -> list[tuple[str, float]]:
23
+ colors = pytest.importorskip("matplotlib.colors")
24
+ return [
25
+ (colors.to_hex(patch.get_facecolor()), patch.get_facecolor()[3])
26
+ for patch in ax.patches
27
+ if patch.get_zorder() == 1
28
+ ]
29
+
30
+
31
+ def _visible_yticklabels(ax) -> list[str]:
32
+ labels = []
33
+ for tick in ax.get_yticklabels():
34
+ _, display_y = ax.transData.transform((0, tick.get_position()[1]))
35
+ labels.append((display_y, tick.get_text()))
36
+ return [label for _, label in sorted(labels, reverse=True)]
37
+
38
+
39
+ def _plot_texts(ax) -> list[str]:
40
+ return [text.get_text() for text in ax.texts]
41
+
42
+
43
+ def test_plot_returns_matplotlib_figure_and_axes() -> None:
44
+ plt = _require_pyplot()
45
+
46
+ data1 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [1, 1, 1, 1]})
47
+ data2 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [0, 0, 1, 1]})
48
+ ship = create_ship(data1, data2, labels=("Before", "After"), y_label="Rate")
49
+
50
+ fig, ax = ship.plot("group")
51
+
52
+ try:
53
+ assert ax.get_title() == "group"
54
+ assert ax.get_ylabel() == "Rate"
55
+ assert [tick.get_text() for tick in ax.get_xticklabels()] == [
56
+ "Before",
57
+ "A",
58
+ "B",
59
+ "After",
60
+ ]
61
+ assert fig is ax.figure
62
+ finally:
63
+ plt.close(fig)
64
+
65
+
66
+ def test_plot_respects_explicit_levels() -> None:
67
+ plt = _require_pyplot()
68
+
69
+ data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
70
+ data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
71
+ ship = create_ship(data1, data2)
72
+
73
+ fig, ax = ship.plot("group", levels=["B", "A"])
74
+
75
+ try:
76
+ assert [tick.get_text() for tick in ax.get_xticklabels()] == [
77
+ "Original",
78
+ "B",
79
+ "A",
80
+ "Refitted",
81
+ ]
82
+ finally:
83
+ plt.close(fig)
84
+
85
+
86
+ def test_plot_validates_main_item() -> None:
87
+ _require_pyplot()
88
+
89
+ data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
90
+ data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
91
+ ship = create_ship(data1, data2)
92
+
93
+ with pytest.raises(ValueError, match="main_item"):
94
+ ship.plot("group", main_item="C")
95
+
96
+
97
+ def test_plot_colors_negative_contributions_red_and_positive_blue() -> None:
98
+ plt = _require_pyplot()
99
+
100
+ data1 = pd.DataFrame(
101
+ {"group": ["A"] * 10 + ["B"] * 10, "y": [0] * 10 + [1] * 10},
102
+ )
103
+ data2 = pd.DataFrame(
104
+ {"group": ["A"] * 10 + ["B"] * 30, "y": [1] * 10 + [0] * 30},
105
+ )
106
+ ship = create_ship(data1, data2)
107
+
108
+ fig, ax = ship.plot("group")
109
+
110
+ try:
111
+ assert _waterfall_colors(ax) == [
112
+ "#00bfc4",
113
+ "#f8766d",
114
+ "#00bfc4",
115
+ "#00bfc4",
116
+ ]
117
+ finally:
118
+ plt.close(fig)
119
+
120
+
121
+ def test_plot_size_bars_use_r_colors_without_alpha() -> None:
122
+ plt = _require_pyplot()
123
+
124
+ data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
125
+ data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
126
+ ship = create_ship(data1, data2)
127
+
128
+ fig, ax = ship.plot("group")
129
+
130
+ try:
131
+ assert _size_bar_styles(ax) == [
132
+ ("#7cae00", 1.0),
133
+ ("#7cae00", 1.0),
134
+ ("#c77cff", 1.0),
135
+ ("#c77cff", 1.0),
136
+ ]
137
+ finally:
138
+ plt.close(fig)
139
+
140
+
141
+ def test_plot_flip_returns_horizontal_matplotlib_plot() -> None:
142
+ plt = _require_pyplot()
143
+
144
+ data1 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [1, 1, 1, 1]})
145
+ data2 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [0, 0, 1, 1]})
146
+ ship = create_ship(data1, data2, labels=("Before", "After"), y_label="Rate")
147
+
148
+ fig, ax = ship.plot_flip("group")
149
+
150
+ try:
151
+ assert ax.get_title() == "group"
152
+ assert ax.get_xlabel() == "Rate"
153
+ assert _visible_yticklabels(ax) == [
154
+ "Before",
155
+ "A",
156
+ "B",
157
+ "After",
158
+ ]
159
+ assert fig is ax.figure
160
+ finally:
161
+ plt.close(fig)
162
+
163
+
164
+ def test_plot_flip_colors_original_negative_contributions_red() -> None:
165
+ plt = _require_pyplot()
166
+
167
+ data1 = pd.DataFrame(
168
+ {"group": ["A"] * 10 + ["B"] * 10, "y": [0] * 10 + [1] * 10},
169
+ )
170
+ data2 = pd.DataFrame(
171
+ {"group": ["A"] * 10 + ["B"] * 30, "y": [1] * 10 + [0] * 30},
172
+ )
173
+ ship = create_ship(data1, data2)
174
+
175
+ fig, ax = ship.plot_flip("group")
176
+
177
+ try:
178
+ assert _waterfall_colors(ax) == [
179
+ "#00bfc4",
180
+ "#00bfc4",
181
+ "#f8766d",
182
+ "#00bfc4",
183
+ ]
184
+ finally:
185
+ plt.close(fig)
186
+
187
+
188
+ def test_plot_flip_labels_use_original_contribution_signs() -> None:
189
+ plt = _require_pyplot()
190
+
191
+ data1 = pd.DataFrame(
192
+ {"group": ["A"] * 10 + ["B"] * 10, "y": [0] * 10 + [1] * 10},
193
+ )
194
+ data2 = pd.DataFrame(
195
+ {"group": ["A"] * 10 + ["B"] * 30, "y": [1] * 10 + [0] * 30},
196
+ )
197
+ ship = create_ship(data1, data2)
198
+
199
+ fig, ax = ship.plot_flip("group")
200
+
201
+ try:
202
+ assert _plot_texts(ax) == ["25", "37.5", "-62.5", "50"]
203
+ finally:
204
+ plt.close(fig)
205
+
206
+
207
+ def test_plot_flip_size_bars_use_r_colors_without_alpha() -> None:
208
+ plt = _require_pyplot()
209
+
210
+ data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
211
+ data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
212
+ ship = create_ship(data1, data2)
213
+
214
+ fig, ax = ship.plot_flip("group")
215
+
216
+ try:
217
+ assert _size_bar_styles(ax) == [
218
+ ("#7cae00", 1.0),
219
+ ("#7cae00", 1.0),
220
+ ("#c77cff", 1.0),
221
+ ("#c77cff", 1.0),
222
+ ]
223
+ finally:
224
+ plt.close(fig)
225
+
226
+
227
+ def test_plot_flip_respects_reversed_explicit_levels() -> None:
228
+ plt = _require_pyplot()
229
+
230
+ data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
231
+ data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
232
+ ship = create_ship(data1, data2)
233
+
234
+ fig, ax = ship.plot_flip("group", levels=["B", "A"])
235
+
236
+ try:
237
+ assert _visible_yticklabels(ax) == [
238
+ "Original",
239
+ "B",
240
+ "A",
241
+ "Refitted",
242
+ ]
243
+ finally:
244
+ plt.close(fig)
245
+
246
+
247
+ def test_plot_flip_validates_main_item() -> None:
248
+ _require_pyplot()
249
+
250
+ data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
251
+ data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
252
+ ship = create_ship(data1, data2)
253
+
254
+ with pytest.raises(ValueError, match="main_item"):
255
+ ship.plot_flip("group", main_item="C")
@@ -1,118 +0,0 @@
1
- import pandas as pd
2
- import pytest
3
-
4
- from theseusplot import create_ship
5
-
6
-
7
- def _require_pyplot():
8
- matplotlib = pytest.importorskip("matplotlib")
9
- matplotlib.use("Agg", force=True)
10
- return pytest.importorskip("matplotlib.pyplot")
11
-
12
-
13
- def test_plot_returns_matplotlib_figure_and_axes() -> None:
14
- plt = _require_pyplot()
15
-
16
- data1 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [1, 1, 1, 1]})
17
- data2 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [0, 0, 1, 1]})
18
- ship = create_ship(data1, data2, labels=("Before", "After"), y_label="Rate")
19
-
20
- fig, ax = ship.plot("group")
21
-
22
- try:
23
- assert ax.get_title() == "group"
24
- assert ax.get_ylabel() == "Rate"
25
- assert [tick.get_text() for tick in ax.get_xticklabels()] == [
26
- "Before",
27
- "A",
28
- "B",
29
- "After",
30
- ]
31
- assert fig is ax.figure
32
- finally:
33
- plt.close(fig)
34
-
35
-
36
- def test_plot_respects_explicit_levels() -> None:
37
- plt = _require_pyplot()
38
-
39
- data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
40
- data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
41
- ship = create_ship(data1, data2)
42
-
43
- fig, ax = ship.plot("group", levels=["B", "A"])
44
-
45
- try:
46
- assert [tick.get_text() for tick in ax.get_xticklabels()] == [
47
- "Original",
48
- "B",
49
- "A",
50
- "Refitted",
51
- ]
52
- finally:
53
- plt.close(fig)
54
-
55
-
56
- def test_plot_validates_main_item() -> None:
57
- _require_pyplot()
58
-
59
- data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
60
- data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
61
- ship = create_ship(data1, data2)
62
-
63
- with pytest.raises(ValueError, match="main_item"):
64
- ship.plot("group", main_item="C")
65
-
66
-
67
- def test_plot_flip_returns_horizontal_matplotlib_plot() -> None:
68
- plt = _require_pyplot()
69
-
70
- data1 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [1, 1, 1, 1]})
71
- data2 = pd.DataFrame({"group": ["A", "A", "B", "B"], "y": [0, 0, 1, 1]})
72
- ship = create_ship(data1, data2, labels=("Before", "After"), y_label="Rate")
73
-
74
- fig, ax = ship.plot_flip("group")
75
-
76
- try:
77
- assert ax.get_title() == "group"
78
- assert ax.get_xlabel() == "Rate"
79
- assert [tick.get_text() for tick in ax.get_yticklabels()] == [
80
- "After",
81
- "B",
82
- "A",
83
- "Before",
84
- ]
85
- assert fig is ax.figure
86
- finally:
87
- plt.close(fig)
88
-
89
-
90
- def test_plot_flip_respects_reversed_explicit_levels() -> None:
91
- plt = _require_pyplot()
92
-
93
- data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
94
- data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
95
- ship = create_ship(data1, data2)
96
-
97
- fig, ax = ship.plot_flip("group", levels=["B", "A"])
98
-
99
- try:
100
- assert [tick.get_text() for tick in ax.get_yticklabels()] == [
101
- "Refitted",
102
- "A",
103
- "B",
104
- "Original",
105
- ]
106
- finally:
107
- plt.close(fig)
108
-
109
-
110
- def test_plot_flip_validates_main_item() -> None:
111
- _require_pyplot()
112
-
113
- data1 = pd.DataFrame({"group": ["A", "B"], "y": [1, 1]})
114
- data2 = pd.DataFrame({"group": ["A", "B"], "y": [0, 1]})
115
- ship = create_ship(data1, data2)
116
-
117
- with pytest.raises(ValueError, match="main_item"):
118
- ship.plot_flip("group", main_item="C")