processbehavior 0.3.1__tar.gz → 0.3.3__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 (209) hide show
  1. {processbehavior-0.3.1 → processbehavior-0.3.3}/CHANGELOG.md +53 -1
  2. {processbehavior-0.3.1 → processbehavior-0.3.3}/PKG-INFO +1 -1
  3. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/__init__.py +1 -1
  4. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/derivations.py +12 -7
  5. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/maximum_information.py +4 -7
  6. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/renderers.py +53 -1
  7. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/spc_constants.py +21 -20
  8. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/missing_values/X_data.parquet +0 -0
  9. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/missing_values/mR_data.parquet +0 -0
  10. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/missing_values/mR_statistics.json +1 -1
  11. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xmr_r/X_data.parquet +0 -0
  12. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xmr_r/X_statistics.json +1 -1
  13. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xmr_r/mR_data.parquet +0 -0
  14. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/pathological_ordering/X_data.parquet +0 -0
  15. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/pathological_ordering/X_statistics.json +1 -1
  16. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/pathological_ordering/mR_data.parquet +0 -0
  17. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/pathological_ordering/mR_statistics.json +1 -1
  18. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/single_obs_strata/X_data.parquet +0 -0
  19. processbehavior-0.3.3/tests/fixtures/golden_masters/single_obs_strata/X_statistics.json +6 -0
  20. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/single_obs_strata/mR_data.parquet +0 -0
  21. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/single_obs_strata/mR_statistics.json +1 -1
  22. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/unstratified_small/X_data.parquet +0 -0
  23. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/unstratified_small/X_statistics.json +1 -1
  24. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/unstratified_small/mR_data.parquet +0 -0
  25. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/unstratified_small/mR_statistics.json +1 -1
  26. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_derivations.py +13 -6
  27. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_derivations_robustness.py +16 -2
  28. processbehavior-0.3.3/tests/test_lane_connectors.py +103 -0
  29. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_spc_constants.py +36 -4
  30. processbehavior-0.3.3/tests/test_vas_chart_constants.py +86 -0
  31. processbehavior-0.3.1/tests/fixtures/golden_masters/single_obs_strata/X_statistics.json +0 -6
  32. {processbehavior-0.3.1 → processbehavior-0.3.3}/.gitignore +0 -0
  33. {processbehavior-0.3.1 → processbehavior-0.3.3}/CITATION.cff +0 -0
  34. {processbehavior-0.3.1 → processbehavior-0.3.3}/CODE_OF_CONDUCT.md +0 -0
  35. {processbehavior-0.3.1 → processbehavior-0.3.3}/CONTRIBUTING.md +0 -0
  36. {processbehavior-0.3.1 → processbehavior-0.3.3}/LICENSE +0 -0
  37. {processbehavior-0.3.1 → processbehavior-0.3.3}/README.md +0 -0
  38. {processbehavior-0.3.1 → processbehavior-0.3.3}/SECURITY.md +0 -0
  39. {processbehavior-0.3.1 → processbehavior-0.3.3}/examples/README.md +0 -0
  40. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/analysis.py +0 -0
  41. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/analysis_dataset.py +0 -0
  42. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/analysis_result.py +0 -0
  43. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/calibration.py +0 -0
  44. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/capability.py +0 -0
  45. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/data_preparation.py +0 -0
  46. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/datasets/__init__.py +0 -0
  47. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/datasets/data/coffee_shop_demo_long.csv +0 -0
  48. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/datasets/loaders.py +0 -0
  49. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/datasets/synthetic.py +0 -0
  50. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/effects_calculator.py +0 -0
  51. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/exceptions.py +0 -0
  52. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/exporters/__init__.py +0 -0
  53. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/exporters/excel_exporter.py +0 -0
  54. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/focused_result.py +0 -0
  55. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/formulation_spec.py +0 -0
  56. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/loss_function.py +0 -0
  57. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/__init__.py +0 -0
  58. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/_math.py +0 -0
  59. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/capability_chart.py +0 -0
  60. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/contracts.py +0 -0
  61. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/control_chart.py +0 -0
  62. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/effects_charts.py +0 -0
  63. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/lane_boundaries.py +0 -0
  64. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/limits.py +0 -0
  65. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/maximum_information_chart.py +0 -0
  66. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/plotter.py +0 -0
  67. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/report.py +0 -0
  68. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/residuals.py +0 -0
  69. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/run_rules_viz.py +0 -0
  70. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/stats_box.py +0 -0
  71. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/themes.py +0 -0
  72. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/x_axis_layout.py +0 -0
  73. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/plotting/zones.py +0 -0
  74. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/process_behavior.py +0 -0
  75. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/py.typed +0 -0
  76. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/residual_calculator.py +0 -0
  77. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/result_signals.py +0 -0
  78. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/result_tabular.py +0 -0
  79. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/sds_detector.py +0 -0
  80. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/series_length.py +0 -0
  81. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/signals/__init__.py +0 -0
  82. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/signals/config.py +0 -0
  83. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/signals/detector.py +0 -0
  84. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/signals/detectors.py +0 -0
  85. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/signals/result.py +0 -0
  86. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/study.py +0 -0
  87. {processbehavior-0.3.1 → processbehavior-0.3.3}/processbehavior/types.py +0 -0
  88. {processbehavior-0.3.1 → processbehavior-0.3.3}/pyproject.toml +0 -0
  89. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/__init__.py +0 -0
  90. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/conftest.py +0 -0
  91. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/bishop_analyses/vassds1analysis.json +0 -0
  92. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/bishop_analyses/vassds2analysis.json +0 -0
  93. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/bishop_analyses/vassds3analysis.json +0 -0
  94. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/coffee_capability_full_baseline.json +0 -0
  95. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/data/FILLWEIGHTDATA_800.csv +0 -0
  96. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/data/coffee_shop_demo_long.csv +0 -0
  97. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/missing_values/X_metadata.json +0 -0
  98. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/missing_values/X_statistics.json +0 -0
  99. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/missing_values/mR_metadata.json +0 -0
  100. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xbar_s/S_data.parquet +0 -0
  101. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xbar_s/S_metadata.json +0 -0
  102. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xbar_s/S_statistics.json +0 -0
  103. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_data.parquet +0 -0
  104. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_metadata.json +0 -0
  105. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_statistics.json +0 -0
  106. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xmr_r/X_metadata.json +0 -0
  107. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xmr_r/mR_metadata.json +0 -0
  108. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/paired_xmr_r/mR_statistics.json +0 -0
  109. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/pathological_ordering/X_metadata.json +0 -0
  110. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/pathological_ordering/mR_metadata.json +0 -0
  111. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/residual_chart/S_data.parquet +0 -0
  112. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/residual_chart/S_metadata.json +0 -0
  113. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/residual_chart/S_statistics.json +0 -0
  114. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/single_obs_strata/X_metadata.json +0 -0
  115. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/single_obs_strata/mR_metadata.json +0 -0
  116. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_balanced/S_data.parquet +0 -0
  117. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_balanced/S_metadata.json +0 -0
  118. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_balanced/S_statistics.json +0 -0
  119. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_balanced/Xbar_data.parquet +0 -0
  120. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_balanced/Xbar_metadata.json +0 -0
  121. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_balanced/Xbar_statistics.json +0 -0
  122. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_uneven/S_data.parquet +0 -0
  123. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_uneven/S_metadata.json +0 -0
  124. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_uneven/S_statistics.json +0 -0
  125. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_uneven/Xbar_data.parquet +0 -0
  126. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_uneven/Xbar_metadata.json +0 -0
  127. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/stratified_uneven/Xbar_statistics.json +0 -0
  128. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/unstratified_small/X_metadata.json +0 -0
  129. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/fixtures/golden_masters/unstratified_small/mR_metadata.json +0 -0
  130. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_analysis_bugs.py +0 -0
  131. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_analysis_dataset.py +0 -0
  132. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_analysis_golden_master.py +0 -0
  133. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_analysis_result_contract.py +0 -0
  134. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_analysis_specifications.py +0 -0
  135. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_api_reference.py +0 -0
  136. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_beyond_limits_vectorized.py +0 -0
  137. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_bin_extremes.py +0 -0
  138. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_bishop_reference.py +0 -0
  139. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_by_parameter.py +0 -0
  140. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_by_permutations.py +0 -0
  141. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_calibration.py +0 -0
  142. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_capability.py +0 -0
  143. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_capability_chart.py +0 -0
  144. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_capability_window.py +0 -0
  145. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_chart_availability.py +0 -0
  146. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_chart_column_contract.py +0 -0
  147. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_chart_parsing.py +0 -0
  148. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_control_chart_figure.py +0 -0
  149. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_data_preparation.py +0 -0
  150. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_datasets.py +0 -0
  151. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_design_state_lineage.py +0 -0
  152. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_documented_rule_coverage.py +0 -0
  153. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_edge_cases.py +0 -0
  154. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_effects_calculator.py +0 -0
  155. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_effects_plotting.py +0 -0
  156. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_encode_rsg_vectorized.py +0 -0
  157. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_excel_export.py +0 -0
  158. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_execute_validation.py +0 -0
  159. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_focus.py +0 -0
  160. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_garbage_character_handling.py +0 -0
  161. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_init_cleaning.py +0 -0
  162. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_lane_boundaries.py +0 -0
  163. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_limits_vectorized.py +0 -0
  164. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_loss_function.py +0 -0
  165. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_maximum_information.py +0 -0
  166. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_na_in_factor_columns.py +0 -0
  167. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_notebook_ergonomics.py +0 -0
  168. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_p0_p2_regressions.py +0 -0
  169. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_performance.py +0 -0
  170. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_phased_limits.py +0 -0
  171. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_plotting.py +0 -0
  172. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_plotting_decorations.py +0 -0
  173. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_plotting_hardening.py +0 -0
  174. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_plotting_xaxis.py +0 -0
  175. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_process_behavior.py +0 -0
  176. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_recentered_mr_fix.py +0 -0
  177. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_reference_integrity.py +0 -0
  178. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_release_gate_0_1_0_checklist.py +0 -0
  179. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_residual_aliases.py +0 -0
  180. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_residual_calculator.py +0 -0
  181. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_residual_labels.py +0 -0
  182. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_residual_scope.py +0 -0
  183. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_resolve_by_grouping.py +0 -0
  184. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_result_signals.py +0 -0
  185. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_result_tabular.py +0 -0
  186. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_sampling_plan.py +0 -0
  187. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_sds.py +0 -0
  188. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_sds_detector.py +0 -0
  189. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_series_length.py +0 -0
  190. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_signal_detectors.py +0 -0
  191. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_signal_result_surface.py +0 -0
  192. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_signals.py +0 -0
  193. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_single_detection.py +0 -0
  194. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_solo_mr_residuals.py +0 -0
  195. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_srp_refactor.py +0 -0
  196. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_strata_companion.py +0 -0
  197. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_strata_sufficiency.py +0 -0
  198. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_stratified_insufficiency.py +0 -0
  199. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_supports_calibration.py +0 -0
  200. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_synthetic.py +0 -0
  201. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_types.py +0 -0
  202. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_validate_by_parameter.py +0 -0
  203. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_weco_integration.py +0 -0
  204. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_why_not.py +0 -0
  205. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_x_axis_layout.py +0 -0
  206. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_xbar_residual_center.py +0 -0
  207. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_xbar_s_options.py +0 -0
  208. {processbehavior-0.3.1 → processbehavior-0.3.3}/tests/test_xbar_stratified.py +0 -0
  209. {processbehavior-0.3.1 → processbehavior-0.3.3}/validation/PBTESTDATABASE_T100.csv +0 -0
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.3] - 2026-09-29
11
+
12
+ ### Changed
13
+ - **Individuals and moving-range limits now use the constants exactly as Bishop's VAS manual
14
+ defines them.** The individuals-chart multiplier is 3/1.128 = 2.659574 (previously the
15
+ rounded 2.66) and the moving-range upper limit is 1 + 3(0.8525)/1.128 = 3.267287 (previously
16
+ 3.268), built from the manual's d₂ = 1.128 and d₃ = 0.8525 (Eq 12.4, 12.5, 12.10, 12.11). The
17
+ manual prints the rounded values; Bishop's VAS software computes them without rounding, and PB's
18
+ X and mR limits now match it digit for digit (Medicare first chart: ±3838.32 and 4715.38).
19
+ Limits move by 0.016% and 0.02%, so a signal changes only for a point within that distance of a
20
+ limit. The d₂ used on the calibration path and by the maximum-information chart is now exactly
21
+ 1.128 (it was 3/2.66 = 1.1278), and `MR_SIGMA_INTERVAL_80` was regenerated: five entries move
22
+ by 0.001. New constant `D3_N2` in `processbehavior.spc_constants`.
23
+
24
+ ### Added
25
+ - Docs: the chart-types guide now explains the three views of a study with factors and
26
+ time (the combined ``by=[]`` chart, the ``phased=True`` view, and full stratification),
27
+ how to read a signal at a subgroup boundary on the combined chart (evidence about the
28
+ traversal, kept in the calculation deliberately because it reproduces Bishop's reference
29
+ limits), and which claims survive reordering the lanes, with the R5 chart's limits
30
+ caveat for DS 2 and 3. The ``phased`` view is documented in the user guide for the first
31
+ time. From the #114 discussion with @rabujamra, whose permutation check is
32
+ ``validation/mr_permutation_invariance.py``.
33
+
34
+ ## [0.3.2] - 2026-09-15
35
+
36
+ ### Fixed
37
+ - **Binning a heavily tied column no longer collapses to one bin.** 0.3.1 capped the
38
+ requested bin count at the number of distinct values *before* taking the quantiles, and on
39
+ a column of 3,000 ones and 1,000 twos the halved quantile positions both landed on 1, so
40
+ one bin fitted where 0.3.0 fitted two. When at least as many bins are requested as there
41
+ are distinct values, the fit now cuts at the midpoints between neighbouring values: one
42
+ bin per distinct value, every value in its own bin, with the message ``requested n bins,
43
+ ties produced k (one per distinct value)``. Requesting fewer bins than distinct values is
44
+ untouched (the original quantile / equal-width path). Found by the app's preview test.
45
+ - **The connecting line on a lane chart no longer crosses lane boundaries.** On an X or mR
46
+ chart with factors and ``by=[]`` (one chart, subgroups side by side), the last point of
47
+ each subgroup was joined to the first point of the next, so every boundary drew a steep
48
+ rise or drop that was only the sort order. The line is now its own trace with a gap at
49
+ each boundary, drawn under the markers; the markers trace keeps hover, legend and
50
+ signal highlighting exactly as before. Single-series and faceted charts are unchanged. #121
51
+
52
+ ### Added
53
+ - Docs: a user-guide page on series length and where sigma comes from (the position, the
54
+ n = 3..30 precision table from ``validation/short_series_bands.py`` with credit to
55
+ @rabujamra, the trending-series and not-one-process cautions, the formulation lesson from
56
+ #114, and the two-audiences position on run rules), and a user-guide page stating the
57
+ derived-variables contract (never raises, what counts as a violation, what bins promise,
58
+ what construction rejects).
59
+
10
60
  ## [0.3.1] - 2026-09-14
11
61
 
12
62
  ### Fixed
@@ -903,7 +953,9 @@ and may change between minor versions until 1.0.
903
953
  - `natsort >= 8.0`
904
954
  - `plotly >= 5.18, < 7`
905
955
 
906
- [Unreleased]: https://github.com/cnicholas/processbehavior/compare/v0.3.1...HEAD
956
+ [Unreleased]: https://github.com/cnicholas/processbehavior/compare/v0.3.3...HEAD
957
+ [0.3.3]: https://github.com/cnicholas/processbehavior/compare/v0.3.2...v0.3.3
958
+ [0.3.2]: https://github.com/cnicholas/processbehavior/compare/v0.3.1...v0.3.2
907
959
  [0.3.1]: https://github.com/cnicholas/processbehavior/compare/v0.3.0...v0.3.1
908
960
  [0.3.0]: https://github.com/cnicholas/processbehavior/compare/v0.2.1...v0.3.0
909
961
  [0.2.1]: https://github.com/cnicholas/processbehavior/compare/v0.2.0...v0.2.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: processbehavior
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Python-native Process Behavior & Control Charts (spec-driven SPC with tidy outputs)
5
5
  Project-URL: Homepage, https://github.com/cnicholas/processbehavior
6
6
  Project-URL: Repository, https://github.com/cnicholas/processbehavior
@@ -47,7 +47,7 @@ its ``.sds`` field. Internally some modules still use the legacy "SDS"
47
47
  prefix (e.g. ``sds_detector.py``); the model itself is three-state.
48
48
  """
49
49
 
50
- __version__ = '0.3.1'
50
+ __version__ = '0.3.3'
51
51
 
52
52
  # Result object
53
53
  from processbehavior.analysis_result import AnalysisResult
@@ -528,19 +528,24 @@ def _fit_edges(spec: Derivation, present_vals: pd.Series):
528
528
  edges = [-math.inf, mu - 2 * sigma, mu - sigma, mu + sigma, mu + 2 * sigma, math.inf]
529
529
  else:
530
530
  n = params['n']
531
- n_distinct = int(present_vals.nunique())
532
- if n_distinct and n > n_distinct:
533
- # More bins than distinct values can only produce empty bins.
534
- message = f'requested {n} bins, only {n_distinct} distinct values; fitted {n_distinct}'
535
- n = n_distinct
536
- if method == 'equal_width':
531
+ distinct = np.unique(present_vals.to_numpy(dtype=float))
532
+ if len(distinct) >= 2 and n >= len(distinct):
533
+ # More bins than distinct values: one bin per distinct value, cut at the
534
+ # midpoints between neighbours. Capping n and re-taking quantiles instead
535
+ # collapsed heavily tied columns (3,000 ones and 1,000 twos fitted one bin);
536
+ # the requested-n quantiles interpolated between the values and left mostly
537
+ # empty bins when n was large.
538
+ mids = (distinct[:-1] + distinct[1:]) / 2.0
539
+ edges = [float(distinct[0]), *[float(m) for m in mids], float(distinct[-1])]
540
+ message = f'requested {n} bins, ties produced {len(distinct)} (one per distinct value)'
541
+ elif method == 'equal_width':
537
542
  lo, hi = float(present_vals.min()), float(present_vals.max())
538
543
  edges = list(np.linspace(lo, hi, n + 1))
539
544
  else: # equal_freq
540
545
  qs = np.linspace(0.0, 1.0, n + 1)
541
546
  edges = list(np.unique(np.quantile(present_vals, qs)))
542
547
  if len(edges) - 1 != n:
543
- message = f'requested {params["n"]} bins, ties produced {len(edges) - 1}'
548
+ message = f'requested {n} bins, ties produced {len(edges) - 1}'
544
549
  return edges, message
545
550
 
546
551
 
@@ -21,7 +21,7 @@ import numpy as np
21
21
  import pandas as pd
22
22
 
23
23
  from .exceptions import ValidationError
24
- from .spc_constants import R_UPPER_LIMIT_MULTIPLIER, XMR_LIMIT_MULTIPLIER
24
+ from .spc_constants import D2_N2, R_UPPER_LIMIT_MULTIPLIER, XMR_LIMIT_MULTIPLIER
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from .analysis_dataset import AnalysisDataSet
@@ -48,9 +48,9 @@ class MaximumInformationResult:
48
48
  sigma_hat : float
49
49
  mR / d2 — noise floor sigma estimate.
50
50
  upl : float
51
- Upper natural process limit (R2 mean + 2.66 * mR).
51
+ Upper natural process limit (R2 mean + E2 * mR, E2 = 3/1.128 ≈ 2.66).
52
52
  lpl : float
53
- Lower natural process limit (R2 mean - 2.66 * mR).
53
+ Lower natural process limit (R2 mean - E2 * mR).
54
54
  n_signals : int
55
55
  Points beyond limits on XmR.
56
56
  round_to : int
@@ -159,9 +159,6 @@ class MaximumInformationResult:
159
159
  # Pure Function
160
160
  # ============================================================================
161
161
 
162
- # d2 for n=2 (moving range of consecutive pairs)
163
- _D2 = 1.128
164
-
165
162
 
166
163
  def assess_maximum_information(
167
164
  ads: AnalysisDataSet,
@@ -204,7 +201,7 @@ def assess_maximum_information(
204
201
  r2_mean = float(np.mean(r2_values))
205
202
  mr_values = np.abs(np.diff(r2_values))
206
203
  r2_mR = float(np.mean(mr_values))
207
- sigma_hat = r2_mR / _D2
204
+ sigma_hat = r2_mR / D2_N2
208
205
 
209
206
  # Limits: mean ± E2 * mR
210
207
  upl = r2_mean + XMR_LIMIT_MULTIPLIER * r2_mR
@@ -35,6 +35,36 @@ if TYPE_CHECKING:
35
35
  # ---------------------------------------------------------------------------
36
36
 
37
37
 
38
+ def _lane_positions(lane_boundaries, chart_name: str, n_rows: int) -> list[int]:
39
+ """0-based row positions where a new lane starts (excluding row 0), for this chart.
40
+
41
+ Accepts the flat ``list[dict]`` form or the per-stratum ``dict`` form of the
42
+ ``lane_boundaries`` metadata; returns ``[]`` when there are no boundaries.
43
+ """
44
+ if not lane_boundaries:
45
+ return []
46
+ if isinstance(lane_boundaries, dict):
47
+ lane_boundaries = lane_boundaries.get(chart_name) or []
48
+ positions = sorted({int(b['position']) for b in lane_boundaries if 0 < int(b['position']) < n_rows})
49
+ return positions
50
+
51
+
52
+ def _with_gaps(values: list, positions: list[int], repeat: bool = False) -> list:
53
+ """Insert a gap entry before each position so Plotly breaks the line there.
54
+
55
+ For ``y`` the gap is ``None`` (a null y breaks the line). For ``x`` pass
56
+ ``repeat=True`` to insert the position's own x value instead, so the x array
57
+ stays numeric/categorical throughout and axis consumers never see a null.
58
+ """
59
+ out: list = []
60
+ cut = set(positions)
61
+ for i, v in enumerate(values):
62
+ if i in cut:
63
+ out.append(v if repeat else None)
64
+ out.append(v)
65
+ return out
66
+
67
+
38
68
  def _build_hover(
39
69
  data: pd.DataFrame,
40
70
  ctx: RenderContext,
@@ -136,10 +166,32 @@ def render_control_chart(
136
166
 
137
167
  # 2. Main data trace
138
168
  customdata, hovertemplate = _build_hover(data, ctx, value_col)
169
+ lane_positions = _lane_positions(spec.lane_boundaries, ctx.chart_name, len(data))
170
+ if lane_positions:
171
+ # Lane chart: the connecting line must not cross a lane boundary. The last point
172
+ # of one subgroup joined to the first of the next drew a steep "event" at every
173
+ # boundary that was only the sort order (#121). Draw the line as its own trace
174
+ # with a gap at each boundary, under the markers; the markers trace keeps the
175
+ # hover, customdata and legend exactly as before.
176
+ _add_trace(
177
+ fig,
178
+ go.Scatter(
179
+ x=_with_gaps(list(x_data), lane_positions, repeat=True),
180
+ y=_with_gaps(list(data[value_col]), lane_positions),
181
+ mode='lines',
182
+ line=dict(color=theme.data_color, width=ctx.line_width),
183
+ opacity=theme.data_opacity,
184
+ hoverinfo='skip',
185
+ showlegend=False,
186
+ name=f'{ctx.chart_name} (lanes)',
187
+ ),
188
+ row,
189
+ col,
190
+ )
139
191
  trace_kw = dict(
140
192
  x=x_data,
141
193
  y=data[value_col],
142
- mode='lines+markers',
194
+ mode='markers' if lane_positions else 'lines+markers',
143
195
  name=ctx.chart_name,
144
196
  marker=dict(size=ctx.marker_size, color=theme.data_color),
145
197
  line=dict(color=theme.data_color, width=ctx.line_width),
@@ -29,15 +29,19 @@ from .exceptions import ValidationError
29
29
  # Control limit multiplier (3-sigma limits are standard in SPC)
30
30
  SIGMA_MULTIPLIER = 3
31
31
 
32
- # E2 constant for XmR charts (n=2, moving range of 2 consecutive observations)
33
- # Used for calculating control limits on individual values
34
- # E2 = d2 / d3 for n=2, where d2 = 1.128 and d3 = 0.8525
35
- XMR_LIMIT_MULTIPLIER = 2.66
32
+ # Moving-range constants for n = 2 (consecutive pairs), as in Bishop's VAS manual:
33
+ # sigma = mR / d2 (Eq 12.4), X limits = X ± 3·mR/d2 (Eq 12.10-12.11), and the
34
+ # moving-range upper limit = (1 + 3·d3/d2)·mR (Eq 12.5). The manual prints the
35
+ # rounded values 2.66 and 3.268; Bishop's VAS software computes them from d2 and
36
+ # d3 without rounding, and so does this library.
37
+ D2_N2 = 1.128
38
+ D3_N2 = 0.8525
36
39
 
37
- # D4 constant for R charts (n=2, range of 2 consecutive observations)
38
- # Used for upper control limit on moving range
39
- # D4 = 1 + 3(d3/d2) for n=2
40
- R_UPPER_LIMIT_MULTIPLIER = 3.268
40
+ # E2 for XmR charts: X ± E2·mR, E2 = 3/d2 (≈ 2.66)
41
+ XMR_LIMIT_MULTIPLIER = SIGMA_MULTIPLIER / D2_N2
42
+
43
+ # D4 for the moving-range chart: upper limit = D4·mR, D4 = 1 + 3·d3/d2 (≈ 3.267)
44
+ R_UPPER_LIMIT_MULTIPLIER = 1 + SIGMA_MULTIPLIER * D3_N2 / D2_N2
41
45
 
42
46
 
43
47
  # ============================================================================
@@ -260,8 +264,8 @@ def calculate_limits(
260
264
  -----
261
265
  Xbar limits: X̄ ± (3 * Wd) / sqrt(n), where Wd = S / c4(n)
262
266
  S limits: S * b3(n) to S * b4(n)
263
- XmR limits: X̄ ± (E2 * mR), where E2 = 2.66
264
- R limits: 0 to mR * D4, where D4 = 3.268
267
+ XmR limits: X̄ ± (E2 * mR), where E2 = 3/d2 = 3/1.128 (≈ 2.66)
268
+ R limits: 0 to mR * D4, where D4 = 1 + 3·d3/d2 = 1 + 3(0.8525)/1.128 (≈ 3.267)
265
269
 
266
270
  References
267
271
  ----------
@@ -414,11 +418,8 @@ def calculate_limits_vectorized(
414
418
  return pd.DataFrame({'lpl': lpl, 'upl': upl}, index=index)
415
419
 
416
420
 
417
- # d2 bias constant for the n=2 moving range, derived from the library's own E2
418
- # (E2 = sigma_multiplier / d2 at n=2, with the default 3-sigma multiplier). Used
419
- # only on the calibration path so calibrated X/mR limits stay internally
420
- # consistent with the data-path XmR/R constants.
421
- D2_N2 = 3.0 / XMR_LIMIT_MULTIPLIER
421
+ # calibrated_limits uses D2_N2 (defined with the XmR constants above), so
422
+ # calibrated X/mR limits stay consistent with the data-path XmR/R constants.
422
423
 
423
424
 
424
425
  def calibrated_limits(
@@ -714,12 +715,12 @@ MR_SIGMA_INTERVAL_80: dict[int, tuple[float, float]] = {
714
715
  9: (0.618, 1.418),
715
716
  10: (0.637, 1.396),
716
717
  11: (0.656, 1.375),
717
- 12: (0.670, 1.360),
718
+ 12: (0.670, 1.359),
718
719
  13: (0.680, 1.344),
719
- 14: (0.694, 1.330),
720
+ 14: (0.694, 1.329),
720
721
  15: (0.703, 1.315),
721
722
  16: (0.714, 1.310),
722
- 17: (0.722, 1.300),
723
+ 17: (0.721, 1.300),
723
724
  18: (0.730, 1.288),
724
725
  19: (0.739, 1.280),
725
726
  20: (0.745, 1.274),
@@ -728,9 +729,9 @@ MR_SIGMA_INTERVAL_80: dict[int, tuple[float, float]] = {
728
729
  23: (0.761, 1.253),
729
730
  24: (0.766, 1.249),
730
731
  25: (0.771, 1.243),
731
- 26: (0.775, 1.238),
732
+ 26: (0.774, 1.238),
732
733
  27: (0.780, 1.234),
733
734
  28: (0.784, 1.229),
734
- 29: (0.787, 1.224),
735
+ 29: (0.787, 1.223),
735
736
  30: (0.790, 1.220),
736
737
  }
@@ -2,5 +2,5 @@
2
2
  "N": 2,
3
3
  "center": 0.707,
4
4
  "lpl": 0.0,
5
- "upl": 2.312
5
+ "upl": 2.311
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "N": 1,
3
3
  "center": 49.085,
4
- "lpl": 46.738,
4
+ "lpl": 46.739,
5
5
  "upl": 51.432
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "N": 1,
3
3
  "center": 49.774,
4
- "lpl": 47.548,
4
+ "lpl": 47.549,
5
5
  "upl": 52.0
6
6
  }
@@ -2,5 +2,5 @@
2
2
  "N": 2,
3
3
  "center": 0.837,
4
4
  "lpl": 0.0,
5
- "upl": 2.735
5
+ "upl": 2.734
6
6
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "N": 1,
3
+ "center": 48.991,
4
+ "lpl": 45.23,
5
+ "upl": 52.753
6
+ }
@@ -2,5 +2,5 @@
2
2
  "N": 2,
3
3
  "center": 1.414,
4
4
  "lpl": 0.0,
5
- "upl": 4.622
5
+ "upl": 4.621
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "N": 1,
3
3
  "center": 49.213,
4
- "lpl": 46.364,
4
+ "lpl": 46.365,
5
5
  "upl": 52.061
6
6
  }
@@ -2,5 +2,5 @@
2
2
  "N": 2,
3
3
  "center": 1.071,
4
4
  "lpl": 0.0,
5
- "upl": 3.5
5
+ "upl": 3.499
6
6
  }
@@ -133,11 +133,18 @@ def test_equal_freq_labels_and_fitted_edges():
133
133
 
134
134
 
135
135
  def test_tie_drop_uses_fitted_count_labels_and_message():
136
+ """Fewer bins than distinct values, on a tied column: the quantile edges collide.
137
+
138
+ Requesting 4 bins on six 1s and one each of 2..5 puts three quartile edges on 1, so
139
+ only 2 bins fit. Labels are keyed on that fitted count, and the message says so.
140
+ (Requesting *at least* as many bins as distinct values now fits one per value; see
141
+ test_derivations_robustness.)
142
+ """
136
143
  tie = pd.Series([1, 1, 1, 1, 1, 1, 2, 3, 4, 5], dtype=float)
137
- r = evaluate(Derivation.bin('t', n=5, bin_labels='ordinal'), tie)
138
- assert r.fitted['n_bins'] == 3 # qcut dropped duplicate edges
139
- assert r.fitted['labels'] == ['Low', 'Medium', 'High'] # keyed on FITTED count, not 5
140
- assert 'requested 5 bins' in r.message and 'produced 3' in r.message
144
+ r = evaluate(Derivation.bin('t', n=4, bin_labels='ordinal'), tie)
145
+ assert r.fitted['n_bins'] == 2 # quantile edges collided on the tied value
146
+ assert r.fitted['labels'] == ['Low', 'High'] # keyed on FITTED count, not 4
147
+ assert 'requested 4 bins' in r.message and 'produced 2' in r.message
141
148
 
142
149
 
143
150
  @pytest.mark.parametrize('method', ['equal_freq', 'equal_width', 'sd'])
@@ -252,8 +259,8 @@ def test_validate_structured_results():
252
259
 
253
260
  def test_validate_label_count_against_fitted_not_requested():
254
261
  df = pd.DataFrame({'t': [1, 1, 1, 1, 1, 1, 2, 3, 4, 5]})
255
- # request 5 bins with 5 labels, but ties produce 3 bins -> mismatch caught
256
- res = validate(Derivation.bin('t', n=5, bin_labels=['a', 'b', 'c', 'd', 'e']), df)
262
+ # request 4 bins with 4 labels, but ties produce 2 bins -> mismatch caught
263
+ res = validate(Derivation.bin('t', n=4, bin_labels=['a', 'b', 'c', 'd']), df)
257
264
  assert res.ok is False
258
265
  assert any(i['code'] == 'label_count' for i in res.issues)
259
266
 
@@ -183,11 +183,25 @@ class TestBinRobustness:
183
183
  r = evaluate(B('x', method='sd'), pd.Series([-0.085, -0.085, -0.085, -0.085000001]))
184
184
  assert r.fitted['n_bins'] == 5 and r.values.notna().all()
185
185
 
186
- def test_more_bins_than_distinct_values_is_capped_with_a_message(self):
186
+ def test_more_bins_than_distinct_values_gives_one_bin_per_value(self):
187
187
  r = evaluate(B('x', n=1000), pd.Series(np.arange(1.0, 21.0)))
188
- assert r.fitted['n_bins'] == 20 and 'only 20 distinct values' in r.message
188
+ assert r.fitted['n_bins'] == 20 and 'ties produced 20 (one per distinct value)' in r.message
189
+ assert r.values.value_counts(sort=False).tolist() == [1] * 20
189
190
  r = evaluate(B('x', method='equal_width', n=9), pd.Series([1.0, 2.0, 3.0]))
190
191
  assert r.fitted['n_bins'] == 3 and r.values.notna().all()
192
+ assert r.fitted['edges'] == [1.0, 1.5, 2.5, 3.0]
193
+
194
+ def test_heavily_tied_column_keeps_one_bin_per_distinct_value(self):
195
+ """Regression: capping n before the quantiles collapsed 3,000 ones + 1,000 twos to one bin."""
196
+ tied = pd.Series([1.0] * 3000 + [2.0] * 1000)
197
+ r = evaluate(B('x', n=4), tied)
198
+ assert r.fitted['n_bins'] == 2 and 'ties' in r.message
199
+ assert r.values.value_counts(sort=False).tolist() == [3000, 1000]
200
+
201
+ def test_fewer_bins_than_distinct_values_is_untouched(self):
202
+ """n below the distinct count takes the original quantile / linspace path."""
203
+ r = evaluate(B('x', n=4), pd.Series(np.arange(1.0, 21.0)))
204
+ assert r.fitted['edges'] == [1.0, 5.75, 10.5, 15.25, 20.0] and r.message is None
191
205
 
192
206
  def test_ordinal_beyond_five_bins_says_so(self):
193
207
  r = evaluate(B('x', n=6, bin_labels='ordinal'), pd.Series(np.arange(1.0, 61.0)))
@@ -0,0 +1,103 @@
1
+ """On a lane chart the connecting line breaks at every lane boundary (#121).
2
+
3
+ One subgroup's last point used to be joined to the next subgroup's first point, so every
4
+ boundary showed a steep rise or drop that was only the sort order. The line is now its own
5
+ trace with a gap at each boundary; the markers trace keeps hover, customdata and legend.
6
+ """
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ import processbehavior as pb
12
+
13
+
14
+ def _lane_study(n_lanes=3, T=4):
15
+ rng = np.random.default_rng(121)
16
+ rows = []
17
+ for k in range(n_lanes):
18
+ base = 100 * (k + 1)
19
+ for t in range(1, T + 1):
20
+ rows.append({'unit': f'U{k + 1}', 't': t, 'y': base + t + rng.normal(0, 0.1)})
21
+ return pb.formulate(pd.DataFrame(rows), response='y', factors=['unit'], time='t')
22
+
23
+
24
+ def _traces(fig):
25
+ return {t.name: t for t in fig.data}
26
+
27
+
28
+ def test_lane_chart_draws_the_line_with_one_gap_per_boundary():
29
+ fig = _lane_study(n_lanes=3, T=4).execute(chart='X', by=[], companion=True).plot(chart='X')
30
+ traces = _traces(fig)
31
+ line = traces['X (lanes)']
32
+ markers = traces['X']
33
+ assert line.mode == 'lines' and markers.mode == 'markers'
34
+ ys = list(line.y)
35
+ assert ys.count(None) == 2, 'three lanes -> two boundaries -> two gaps'
36
+ assert len([y for y in ys if y is not None]) == 12 and len(markers.y) == 12
37
+ # The gaps sit exactly at the lane starts (positions 4 and 8).
38
+ assert [i for i, y in enumerate(ys) if y is None] == [4, 9]
39
+
40
+
41
+ def test_lines_never_join_two_lanes():
42
+ """No drawn segment spans a boundary: every consecutive non-gap pair is within a lane."""
43
+ fig = _lane_study(n_lanes=4, T=3).execute(chart='X', by=[], companion=True).plot(chart='X')
44
+ line = _traces(fig)['X (lanes)']
45
+ ys = list(line.y)
46
+ # Lanes are at 100, 200, 300, 400 (+ small t); any segment crossing a lane would jump ~100.
47
+ for a, b in zip(ys, ys[1:], strict=False):
48
+ if a is not None and b is not None:
49
+ assert abs(a - b) < 10, (a, b)
50
+
51
+
52
+ def test_markers_keep_hover_and_the_line_has_none():
53
+ fig = _lane_study().execute(chart='X', by=[], companion=True).plot(chart='X')
54
+ traces = _traces(fig)
55
+ assert traces['X'].hovertemplate
56
+ assert traces['X (lanes)'].hoverinfo == 'skip' and traces['X (lanes)'].showlegend is False
57
+
58
+
59
+ def test_single_series_chart_is_unchanged():
60
+ """No lanes -> the original single lines+markers trace, no extra trace."""
61
+ df = pd.DataFrame({'t': range(1, 13), 'y': np.arange(12, dtype=float)})
62
+ fig = pb.formulate(df, response='y', time='t').execute(chart='X', companion=True).plot(chart='X')
63
+ names = [t.name for t in fig.data]
64
+ assert 'X (lanes)' not in names
65
+ assert _traces(fig)['X'].mode == 'lines+markers'
66
+
67
+
68
+ def test_faceted_charts_are_unchanged():
69
+ """by=['unit'] gives one panel per lane: no boundaries inside a panel, so no gaps."""
70
+ fig = _lane_study().execute(chart='X', by=['unit'], companion=True).plot(chart='X', facet=True, ncols=3)
71
+ assert not [t for t in fig.data if t.name and t.name.endswith('(lanes)')]
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # The helpers, directly: both boundary shapes, out-of-range positions, both gap modes
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ def test_lane_positions_accepts_flat_and_per_stratum_boundaries():
80
+ from processbehavior.plotting.renderers import _lane_positions
81
+
82
+ flat = [{'position': 4, 'label': 'B'}, {'position': 8, 'label': 'C'}]
83
+ assert _lane_positions(flat, 'X', n_rows=12) == [4, 8]
84
+ per_stratum = {'X': flat, 'mR': [{'position': 3}]}
85
+ assert _lane_positions(per_stratum, 'X', n_rows=12) == [4, 8]
86
+ assert _lane_positions(per_stratum, 'mR', n_rows=12) == [3]
87
+ assert _lane_positions(per_stratum, 'Histogram', n_rows=12) == []
88
+ assert _lane_positions(None, 'X', n_rows=12) == [] and _lane_positions([], 'X', n_rows=12) == []
89
+
90
+
91
+ def test_lane_positions_ignores_row_zero_and_out_of_range():
92
+ from processbehavior.plotting.renderers import _lane_positions
93
+
94
+ raw = [{'position': 0}, {'position': 5}, {'position': 5}, {'position': 12}, {'position': 99}]
95
+ assert _lane_positions(raw, 'X', n_rows=12) == [5]
96
+
97
+
98
+ def test_with_gaps_inserts_none_for_y_and_repeats_x():
99
+ from processbehavior.plotting.renderers import _with_gaps
100
+
101
+ assert _with_gaps([10, 11, 12, 13], [2]) == [10, 11, None, 12, 13]
102
+ assert _with_gaps(['a', 'b', 'c', 'd'], [2], repeat=True) == ['a', 'b', 'c', 'c', 'd']
103
+ assert _with_gaps([1, 2, 3], []) == [1, 2, 3]
@@ -12,6 +12,10 @@ import pandas as pd
12
12
  import pytest
13
13
 
14
14
  from processbehavior.spc_constants import (
15
+ D2_N2,
16
+ D3_N2,
17
+ R_UPPER_LIMIT_MULTIPLIER,
18
+ XMR_LIMIT_MULTIPLIER,
15
19
  b3,
16
20
  b4,
17
21
  c4,
@@ -121,6 +125,34 @@ def test_b3_b4_raises_on_invalid_n(func):
121
125
  func(1)
122
126
 
123
127
 
128
+ # ============================================================================
129
+ # Test: moving-range constants (n = 2)
130
+ # ============================================================================
131
+
132
+
133
+ class TestMovingRangeConstants:
134
+ """The XmR and mR constants follow Bishop's VAS manual (Eq 12.4, 12.5, 12.10, 12.11)
135
+ and are computed from d2 = 1.128 and d3 = 0.8525 without rounding, as VAS does."""
136
+
137
+ def test_building_blocks_are_the_manuals(self):
138
+ assert D2_N2 == 1.128
139
+ assert D3_N2 == 0.8525
140
+
141
+ def test_multipliers_are_the_manuals_formulas(self):
142
+ assert XMR_LIMIT_MULTIPLIER == 3 / D2_N2
143
+ assert R_UPPER_LIMIT_MULTIPLIER == 1 + 3 * D3_N2 / D2_N2
144
+
145
+ def test_multipliers_match_the_vas_software(self):
146
+ # Measured from Bishop's VAS charts: Medicare 3838.32 / 1443.21 and 4715.38 / 1443.21.
147
+ assert abs(XMR_LIMIT_MULTIPLIER - 2.659574) < 1e-6
148
+ assert abs(R_UPPER_LIMIT_MULTIPLIER - 3.267287) < 1e-6
149
+
150
+ def test_multipliers_round_to_the_manuals_printed_values(self):
151
+ # The manual prints 2.66 and 3.268 (3.686/1.128, with 3.686 itself rounded).
152
+ assert round(XMR_LIMIT_MULTIPLIER, 2) == 2.66
153
+ assert abs(R_UPPER_LIMIT_MULTIPLIER - 3.268) < 0.001
154
+
155
+
124
156
  # ============================================================================
125
157
  # Test: calculate_limits
126
158
  # ============================================================================
@@ -133,10 +165,10 @@ def test_b3_b4_raises_on_invalid_n(func):
133
165
  ('Xbar', dict(mean=10.0, sd=0.5, N=5), 9.286, 10.714, 0.01, 0.01),
134
166
  # S: sd=0.5, N=5 → LPL=0.5*b3(5)=0, UPL=0.5*b4(5)≈1.044
135
167
  ('S', dict(sd=0.5, N=5), 0.0, 1.044, 0.001, 0.01),
136
- # XmR: mean=10, mR=0.3 → 10±2.66*0.3=10±0.798
137
- ('XmR', dict(mean=10.0, mR=0.3), 10.0 - 2.66 * 0.3, 10.0 + 2.66 * 0.3, 0.001, 0.001),
138
- # R: mR=0.3 → LPL=0, UPL=0.3*3.268
139
- ('R', dict(mR=0.3), 0.0, 0.3 * 3.268, 0.001, 0.001),
168
+ # XmR: mean=10, mR=0.3 → 10 ± (3/1.128)·0.3 = 10 ± 0.7979
169
+ ('XmR', dict(mean=10.0, mR=0.3), 10.0 - 3 / 1.128 * 0.3, 10.0 + 3 / 1.128 * 0.3, 1e-12, 1e-12),
170
+ # R: mR=0.3 → LPL=0, UPL = (1 + 3·0.8525/1.128)·0.3
171
+ ('R', dict(mR=0.3), 0.0, (1 + 3 * 0.8525 / 1.128) * 0.3, 1e-12, 1e-12),
140
172
  ],
141
173
  ids=['Xbar', 'S', 'XmR', 'R'],
142
174
  )