processbehavior 0.2.1__tar.gz → 0.3.0__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 (205) hide show
  1. {processbehavior-0.2.1 → processbehavior-0.3.0}/CHANGELOG.md +59 -1
  2. {processbehavior-0.2.1 → processbehavior-0.3.0}/PKG-INFO +2 -2
  3. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/__init__.py +1 -1
  4. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/analysis_dataset.py +1 -1
  5. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/data_preparation.py +33 -1
  6. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/datasets/synthetic.py +2 -2
  7. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/derivations.py +15 -3
  8. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/residual_calculator.py +9 -8
  9. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/sds_detector.py +24 -22
  10. processbehavior-0.3.0/processbehavior/series_length.py +105 -0
  11. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/spc_constants.py +44 -0
  12. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/study.py +42 -0
  13. {processbehavior-0.2.1 → processbehavior-0.3.0}/pyproject.toml +1 -1
  14. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_derivations.py +34 -0
  15. processbehavior-0.3.0/tests/test_na_in_factor_columns.py +96 -0
  16. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_residual_calculator.py +8 -8
  17. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_sds_detector.py +34 -6
  18. processbehavior-0.3.0/tests/test_series_length.py +153 -0
  19. {processbehavior-0.2.1 → processbehavior-0.3.0}/.gitignore +0 -0
  20. {processbehavior-0.2.1 → processbehavior-0.3.0}/CITATION.cff +0 -0
  21. {processbehavior-0.2.1 → processbehavior-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  22. {processbehavior-0.2.1 → processbehavior-0.3.0}/CONTRIBUTING.md +0 -0
  23. {processbehavior-0.2.1 → processbehavior-0.3.0}/LICENSE +0 -0
  24. {processbehavior-0.2.1 → processbehavior-0.3.0}/README.md +0 -0
  25. {processbehavior-0.2.1 → processbehavior-0.3.0}/SECURITY.md +0 -0
  26. {processbehavior-0.2.1 → processbehavior-0.3.0}/examples/README.md +0 -0
  27. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/analysis.py +0 -0
  28. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/analysis_result.py +0 -0
  29. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/calibration.py +0 -0
  30. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/capability.py +0 -0
  31. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/datasets/__init__.py +0 -0
  32. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/datasets/data/coffee_shop_demo_long.csv +0 -0
  33. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/datasets/loaders.py +0 -0
  34. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/effects_calculator.py +0 -0
  35. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/exceptions.py +0 -0
  36. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/exporters/__init__.py +0 -0
  37. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/exporters/excel_exporter.py +0 -0
  38. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/focused_result.py +0 -0
  39. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/formulation_spec.py +0 -0
  40. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/loss_function.py +0 -0
  41. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/maximum_information.py +0 -0
  42. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/__init__.py +0 -0
  43. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/_math.py +0 -0
  44. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/capability_chart.py +0 -0
  45. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/contracts.py +0 -0
  46. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/control_chart.py +0 -0
  47. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/effects_charts.py +0 -0
  48. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/lane_boundaries.py +0 -0
  49. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/limits.py +0 -0
  50. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/maximum_information_chart.py +0 -0
  51. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/plotter.py +0 -0
  52. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/renderers.py +0 -0
  53. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/report.py +0 -0
  54. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/residuals.py +0 -0
  55. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/run_rules_viz.py +0 -0
  56. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/stats_box.py +0 -0
  57. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/themes.py +0 -0
  58. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/x_axis_layout.py +0 -0
  59. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/plotting/zones.py +0 -0
  60. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/process_behavior.py +0 -0
  61. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/py.typed +0 -0
  62. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/result_signals.py +0 -0
  63. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/result_tabular.py +0 -0
  64. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/signals/__init__.py +0 -0
  65. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/signals/config.py +0 -0
  66. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/signals/detector.py +0 -0
  67. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/signals/detectors.py +0 -0
  68. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/signals/result.py +0 -0
  69. {processbehavior-0.2.1 → processbehavior-0.3.0}/processbehavior/types.py +0 -0
  70. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/__init__.py +0 -0
  71. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/conftest.py +0 -0
  72. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/bishop_analyses/vassds1analysis.json +0 -0
  73. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/bishop_analyses/vassds2analysis.json +0 -0
  74. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/bishop_analyses/vassds3analysis.json +0 -0
  75. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/coffee_capability_full_baseline.json +0 -0
  76. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/data/FILLWEIGHTDATA_800.csv +0 -0
  77. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/data/coffee_shop_demo_long.csv +0 -0
  78. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/missing_values/X_data.parquet +0 -0
  79. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/missing_values/X_metadata.json +0 -0
  80. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/missing_values/X_statistics.json +0 -0
  81. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/missing_values/mR_data.parquet +0 -0
  82. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/missing_values/mR_metadata.json +0 -0
  83. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/missing_values/mR_statistics.json +0 -0
  84. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xbar_s/S_data.parquet +0 -0
  85. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xbar_s/S_metadata.json +0 -0
  86. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xbar_s/S_statistics.json +0 -0
  87. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_data.parquet +0 -0
  88. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_metadata.json +0 -0
  89. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_statistics.json +0 -0
  90. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xmr_r/X_data.parquet +0 -0
  91. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xmr_r/X_metadata.json +0 -0
  92. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xmr_r/X_statistics.json +0 -0
  93. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xmr_r/mR_data.parquet +0 -0
  94. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xmr_r/mR_metadata.json +0 -0
  95. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/paired_xmr_r/mR_statistics.json +0 -0
  96. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/pathological_ordering/X_data.parquet +0 -0
  97. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/pathological_ordering/X_metadata.json +0 -0
  98. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/pathological_ordering/X_statistics.json +0 -0
  99. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/pathological_ordering/mR_data.parquet +0 -0
  100. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/pathological_ordering/mR_metadata.json +0 -0
  101. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/pathological_ordering/mR_statistics.json +0 -0
  102. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/residual_chart/S_data.parquet +0 -0
  103. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/residual_chart/S_metadata.json +0 -0
  104. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/residual_chart/S_statistics.json +0 -0
  105. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/single_obs_strata/X_data.parquet +0 -0
  106. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/single_obs_strata/X_metadata.json +0 -0
  107. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/single_obs_strata/X_statistics.json +0 -0
  108. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/single_obs_strata/mR_data.parquet +0 -0
  109. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/single_obs_strata/mR_metadata.json +0 -0
  110. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/single_obs_strata/mR_statistics.json +0 -0
  111. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_balanced/S_data.parquet +0 -0
  112. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_balanced/S_metadata.json +0 -0
  113. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_balanced/S_statistics.json +0 -0
  114. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_balanced/Xbar_data.parquet +0 -0
  115. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_balanced/Xbar_metadata.json +0 -0
  116. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_balanced/Xbar_statistics.json +0 -0
  117. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_uneven/S_data.parquet +0 -0
  118. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_uneven/S_metadata.json +0 -0
  119. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_uneven/S_statistics.json +0 -0
  120. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_uneven/Xbar_data.parquet +0 -0
  121. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_uneven/Xbar_metadata.json +0 -0
  122. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/stratified_uneven/Xbar_statistics.json +0 -0
  123. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/unstratified_small/X_data.parquet +0 -0
  124. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/unstratified_small/X_metadata.json +0 -0
  125. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/unstratified_small/X_statistics.json +0 -0
  126. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/unstratified_small/mR_data.parquet +0 -0
  127. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/unstratified_small/mR_metadata.json +0 -0
  128. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/fixtures/golden_masters/unstratified_small/mR_statistics.json +0 -0
  129. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_analysis_bugs.py +0 -0
  130. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_analysis_dataset.py +0 -0
  131. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_analysis_golden_master.py +0 -0
  132. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_analysis_result_contract.py +0 -0
  133. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_analysis_specifications.py +0 -0
  134. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_api_reference.py +0 -0
  135. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_beyond_limits_vectorized.py +0 -0
  136. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_bin_extremes.py +0 -0
  137. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_bishop_reference.py +0 -0
  138. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_by_parameter.py +0 -0
  139. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_by_permutations.py +0 -0
  140. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_calibration.py +0 -0
  141. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_capability.py +0 -0
  142. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_capability_chart.py +0 -0
  143. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_capability_window.py +0 -0
  144. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_chart_availability.py +0 -0
  145. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_chart_column_contract.py +0 -0
  146. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_chart_parsing.py +0 -0
  147. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_control_chart_figure.py +0 -0
  148. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_data_preparation.py +0 -0
  149. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_datasets.py +0 -0
  150. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_design_state_lineage.py +0 -0
  151. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_documented_rule_coverage.py +0 -0
  152. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_edge_cases.py +0 -0
  153. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_effects_calculator.py +0 -0
  154. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_effects_plotting.py +0 -0
  155. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_encode_rsg_vectorized.py +0 -0
  156. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_excel_export.py +0 -0
  157. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_execute_validation.py +0 -0
  158. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_focus.py +0 -0
  159. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_garbage_character_handling.py +0 -0
  160. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_init_cleaning.py +0 -0
  161. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_lane_boundaries.py +0 -0
  162. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_limits_vectorized.py +0 -0
  163. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_loss_function.py +0 -0
  164. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_maximum_information.py +0 -0
  165. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_notebook_ergonomics.py +0 -0
  166. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_p0_p2_regressions.py +0 -0
  167. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_performance.py +0 -0
  168. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_phased_limits.py +0 -0
  169. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_plotting.py +0 -0
  170. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_plotting_decorations.py +0 -0
  171. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_plotting_hardening.py +0 -0
  172. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_plotting_xaxis.py +0 -0
  173. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_process_behavior.py +0 -0
  174. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_recentered_mr_fix.py +0 -0
  175. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_reference_integrity.py +0 -0
  176. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_release_gate_0_1_0_checklist.py +0 -0
  177. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_residual_aliases.py +0 -0
  178. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_residual_labels.py +0 -0
  179. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_residual_scope.py +0 -0
  180. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_resolve_by_grouping.py +0 -0
  181. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_result_signals.py +0 -0
  182. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_result_tabular.py +0 -0
  183. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_sampling_plan.py +0 -0
  184. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_sds.py +0 -0
  185. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_signal_detectors.py +0 -0
  186. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_signal_result_surface.py +0 -0
  187. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_signals.py +0 -0
  188. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_single_detection.py +0 -0
  189. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_solo_mr_residuals.py +0 -0
  190. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_spc_constants.py +0 -0
  191. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_srp_refactor.py +0 -0
  192. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_strata_companion.py +0 -0
  193. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_strata_sufficiency.py +0 -0
  194. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_stratified_insufficiency.py +0 -0
  195. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_supports_calibration.py +0 -0
  196. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_synthetic.py +0 -0
  197. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_types.py +0 -0
  198. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_validate_by_parameter.py +0 -0
  199. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_weco_integration.py +0 -0
  200. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_why_not.py +0 -0
  201. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_x_axis_layout.py +0 -0
  202. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_xbar_residual_center.py +0 -0
  203. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_xbar_s_options.py +0 -0
  204. {processbehavior-0.2.1 → processbehavior-0.3.0}/tests/test_xbar_stratified.py +0 -0
  205. {processbehavior-0.2.1 → processbehavior-0.3.0}/validation/PBTESTDATABASE_T100.csv +0 -0
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-09-14
11
+
12
+ ### Added
13
+ - **The design report states where sigma comes from at the observed structure and
14
+ series length.** One line under ``Structure``: ``Series length: T=4. Sigma for X/mR
15
+ rests on 3 moving ranges.`` With every cell replicated it reads ``Series length: T=1.
16
+ Sigma rests on within-cell replication.`` — a single time period is a complete study.
17
+ Exposed as ``study.series_length`` (``SeriesLengthPrecision``, frozen) and
18
+ ``study.series_length_description``. The result also carries ``within_cell_df`` and,
19
+ for the moving-range case, ``mr_interval_80`` — the p10/p90 of MRbar/d2 by n = 3..30
20
+ from ``MR_SIGMA_INTERVAL_80`` (``spc_constants``), generated by
21
+ ``validation/short_series_bands.py`` (contributed by @rabujamra in #114 / #119) — for
22
+ callers who want the numbers; the report prints only the sentence. It is a fact beside
23
+ the design state, not a judgment on it: no threshold, label, or warning, and the design
24
+ state, recommended chart and analysis menu do not move with T (pinned by test). Closes
25
+ the series-length half of #114.
26
+
27
+ ### Changed
28
+ - **The R2 method for partial replication is reported as ``ma2``, not ``hybrid``.** The
29
+ docs described a per-cell hybrid for design state 3 (exact deviation where a cell has
30
+ n ≥ 2, moving average where n = 1) and said it was validated by Monte Carlo. The code
31
+ has not done that since cb037f8 (2026-03-05): with any singleton cell, R2 is Bishop's
32
+ ungrouped 2-point moving average over the full canonical sequence (Eq 13.7–13.9) for
33
+ every observation, and the 89 ADS 3 reference assertions hold against that. Only the word
34
+ survived, in six docs pages (three of which described it three different ways), the
35
+ detector's method token, the analysis-plan limitation text, two docstrings and two test
36
+ names. All now say what the code does. ``R2Method`` is ``Literal['exact', 'ma2']``;
37
+ ``SDSRegistry.get_r2_method`` returns ``'ma2'`` where it returned ``'hybrid'``;
38
+ ``get_sds_characteristics`` and ``SDSAnalysisPlan.residual_calculation_method`` use the
39
+ same two spellings in place of ``'within_cell'``, ``'moving_average'`` and ``'hybrid'``.
40
+ No arithmetic changed; none of these names is exported from the package top level.
41
+
42
+ ### Dependencies
43
+ - ``plotly>=5.18,<8`` (was ``<7``). plotly 7.0.0 (2026-08-25) was tested against the full
44
+ suite before the bound moved: its breaking changes (mapbox traces, decimal-fraction
45
+ ``rgb()`` strings, ``hsv()``, Kaleido < 1.0, figure_factory) touch nothing here. #116.
46
+
47
+ ### Fixed
48
+ - **A missing value in a factor column no longer makes a two-factor study fail.** With one
49
+ factor, rows with a blank factor value were dropped silently; with two or more, the
50
+ composite subgroup label was built before that drop and ``formulate`` raised
51
+ ``ValidationError: Cannot build RSG ... missing values in factor columns``. The same
52
+ file formulated with either factor alone and failed with both (a 85,101-row survey
53
+ file with 7,980 blank ``SURVEY QUESTION`` rows). Rows with no value in any factor
54
+ column now leave the analysis before the label is built, for any number of factors,
55
+ and the library says so with a ``ProcessBehaviorWarning`` naming the count per
56
+ factor. Response and time missing values are handled as before.
57
+ - **``evaluate`` no longer raises on a constant column.** Binning a column with no spread
58
+ (every value identical) returned the documented no-spread result for ``equal_freq`` but
59
+ raised ``ValueError: Bin edges must be unique`` from ``pd.cut`` for ``equal_width`` and
60
+ ``sd``, because their fitted edges were identical yet finite and slipped past the guard.
61
+ The derivations module promises ``evaluate`` never raises on a routine state; a constant
62
+ column is one. All three methods now return the same no-spread result, whose ``fitted``
63
+ dict also carries ``n_bins: 0``, ``edges: []`` and ``labels: []`` so callers see one
64
+ shape. Explicit ``breaks`` on a constant column still bin. Found through the app on a
65
+ single-time-point file whose TIME column was a constant.
66
+
10
67
  ## [0.2.1] - 2026-09-09
11
68
 
12
69
  ### Fixed
@@ -812,7 +869,8 @@ and may change between minor versions until 1.0.
812
869
  - `natsort >= 8.0`
813
870
  - `plotly >= 5.18, < 7`
814
871
 
815
- [Unreleased]: https://github.com/cnicholas/processbehavior/compare/v0.2.1...HEAD
872
+ [Unreleased]: https://github.com/cnicholas/processbehavior/compare/v0.3.0...HEAD
873
+ [0.3.0]: https://github.com/cnicholas/processbehavior/compare/v0.2.1...v0.3.0
816
874
  [0.2.1]: https://github.com/cnicholas/processbehavior/compare/v0.2.0...v0.2.1
817
875
  [0.2.0]: https://github.com/cnicholas/processbehavior/compare/v0.1.1...v0.2.0
818
876
  [0.1.1]: https://github.com/cnicholas/processbehavior/compare/v0.1.0...v0.1.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: processbehavior
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -28,7 +28,7 @@ Requires-Python: >=3.10
28
28
  Requires-Dist: natsort>=8.0.0
29
29
  Requires-Dist: numpy>=1.23
30
30
  Requires-Dist: pandas<3,>=2.0
31
- Requires-Dist: plotly<7,>=5.18
31
+ Requires-Dist: plotly<8,>=5.18
32
32
  Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: filelock>=3.20.3; extra == 'dev'
@@ -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.2.1'
50
+ __version__ = '0.3.0'
51
51
 
52
52
  # Result object
53
53
  from processbehavior.analysis_result import AnalysisResult
@@ -183,7 +183,7 @@ class AnalysisDataSet:
183
183
  - n_cell_min: minimum observations per cell
184
184
  - n_cell_max: maximum observations per cell
185
185
  - K_obs: number of unique factor levels observed
186
- - r2_method: 'exact', 'ma2', or 'hybrid'
186
+ - r2_method: 'exact' or 'ma2'
187
187
  Returns None if structure stats haven't been computed.
188
188
  """
189
189
  if self._structure_stats is None:
@@ -16,6 +16,7 @@ Follows the Pythonic Hadley philosophy:
16
16
  from __future__ import annotations
17
17
 
18
18
  import logging
19
+ import warnings
19
20
  from functools import reduce
20
21
  from typing import TYPE_CHECKING
21
22
 
@@ -24,7 +25,7 @@ import pandas as pd
24
25
  from natsort import natsorted
25
26
  from pandas.api.types import is_numeric_dtype
26
27
 
27
- from .exceptions import ColumnNotFoundError, FactorNotFoundError, ValidationError
28
+ from .exceptions import ColumnNotFoundError, FactorNotFoundError, ProcessBehaviorWarning, ValidationError
28
29
 
29
30
  if TYPE_CHECKING:
30
31
  from .formulation_spec import FormulationSpec
@@ -281,6 +282,13 @@ class DataPreparation:
281
282
 
282
283
  # Add composite grouping variable if needed
283
284
  if spec.has_grouping:
285
+ # A row with no value in a factor column belongs to no cell, so it leaves the
286
+ # analysis here — for one factor or many. The single-factor path used to drop
287
+ # such rows silently via the rsg dropna below; the multi-factor path raised
288
+ # from _add_composite_column before reaching it, so the same file formulated
289
+ # with one factor and failed with two (Tom's survey file, 7,980 blank
290
+ # SURVEY QUESTION rows). Both paths now drop first and say so.
291
+ out = self._drop_rows_missing_factor_values(out, spec)
284
292
  out = self._add_grouping_column(out, spec)
285
293
 
286
294
  # Drop rows with missing values in analysis-critical columns BEFORE
@@ -533,6 +541,30 @@ class DataPreparation:
533
541
  # Private Helper Methods
534
542
  # ========================================================================
535
543
 
544
+ def _drop_rows_missing_factor_values(self, df: pd.DataFrame, spec: FormulationSpec) -> pd.DataFrame:
545
+ """
546
+ Drop rows with a missing value in any factor column, and report it.
547
+
548
+ Reported as a ``ProcessBehaviorWarning`` (the house style for "the
549
+ library changed your data on your behalf"), naming the count per
550
+ factor, so an analyst sees that 7,980 rows left the study rather than
551
+ discovering a smaller N later.
552
+ """
553
+ factor_cols = spec.rsg_vars_list
554
+ missing = df[factor_cols].isna()
555
+ if not missing.any().any():
556
+ return df
557
+ per_col = {col: int(n) for col, n in missing.sum().items() if n}
558
+ n_rows = int(missing.any(axis=1).sum())
559
+ warnings.warn(
560
+ f'Dropped {n_rows:,} of {len(df):,} rows with no value in a factor column: '
561
+ + ', '.join(f'{col} ({n:,} missing)' for col, n in per_col.items())
562
+ + '. A row with no factor value belongs to no cell, so it is excluded from the analysis.',
563
+ ProcessBehaviorWarning,
564
+ stacklevel=6,
565
+ )
566
+ return df.loc[~missing.any(axis=1)]
567
+
536
568
  def _add_grouping_column(self, df: pd.DataFrame, spec: FormulationSpec) -> pd.DataFrame:
537
569
  """
538
570
  Add composite grouping column (e.g., 'lane_head').
@@ -416,13 +416,13 @@ def make_sds3( # noqa: C901
416
416
  Characteristics:
417
417
  ---------------
418
418
  - Some cells have n=1, others have n≥2
419
- - Requires hybrid R2 estimation
419
+ - Mixed cell sizes: R2 by MA2 across the full sequence
420
420
  - Reflects real-world data collection reality
421
421
  - Most challenging to analyze correctly
422
422
 
423
423
  Use Cases:
424
424
  ---------
425
- - Testing hybrid variance estimation algorithms
425
+ - Testing R2 selection with mixed cell sizes
426
426
  - Demonstrating real-world data structures
427
427
  - Validating robustness to missing data
428
428
  - Development of SDS detection logic
@@ -473,11 +473,23 @@ def _evaluate_bin(spec: Derivation, col: pd.Series) -> EvalResult:
473
473
 
474
474
  edges, fit_msg = _fit_edges(spec, x[present])
475
475
 
476
- # Degenerate fit (no spread) — cannot bin.
477
- if len(edges) < 2 or any(not math.isfinite(e) for e in edges[1:-1]):
476
+ # Degenerate fit (no spread) — cannot bin. A constant column produces three shapes,
477
+ # one per method: a single edge (equal_freq, after np.unique collapses the quantiles),
478
+ # identical finite edges (equal_width, linspace(lo, lo)), and a zero-sigma sd fit
479
+ # ([-inf, mu, mu, mu, mu, inf]). pd.cut raises "Bin edges must be unique" on the latter
480
+ # two, and evaluate() promises never to raise on a routine state, so all three return
481
+ # the same no-spread result. User-supplied breaks are validated ascending elsewhere and
482
+ # bin a constant column fine (everything lands in one interval), so they never trip this.
483
+ degenerate = (
484
+ len(edges) < 2
485
+ or any(not math.isfinite(e) for e in edges[1:-1])
486
+ or any(b <= a for a, b in zip(edges, edges[1:], strict=False)) # not strictly increasing
487
+ )
488
+ if degenerate:
478
489
  return EvalResult(
479
490
  values=pd.Series(pd.Categorical([np.nan] * len(x)), index=x.index),
480
- n_invalid=0, invalid_index=empty_index, fitted={'method': params['method']},
491
+ n_invalid=0, invalid_index=empty_index,
492
+ fitted={'method': params['method'], 'n_bins': 0, 'edges': [], 'labels': []},
481
493
  message='column has no spread; cannot bin',
482
494
  )
483
495
 
@@ -338,14 +338,14 @@ def calculate_r2(df: pd.DataFrame, y: str, r2_method: R2Method, n_per_cell: pd.S
338
338
  R2 is the ONLY structure-dependent residual. The method is determined by
339
339
  observed cell sizes (via SDSRegistry.get_r2_method()):
340
340
 
341
- - exact (state 1): R2 = Y - Ȳ_kt (Eq 59), all cells have n >= 2
342
- - ma2 (states 2 & 3): R2 = (Y_j - Y_{j-1}) / 2 (Eq 13.8-13.9),
341
+ - exact (ADS 1): R2 = Y - Ȳ_kt (Eq 59), every cell has n >= 2
342
+ - ma2 (ADS 2 and 3): R2 = (Y_j - Y_{j-1}) / 2 (Eq 13.7-13.9),
343
343
  any cell has n = 1
344
344
 
345
345
  When any cell has n=1, MA2 is applied to ALL observations across the
346
- entire canonical-sorted stream — no grouping by rsg_key, no hybrid
347
- per-cell selection. Bishop Eq 13.7-13.9 specify j=2,...,J with no
348
- grouping; only j=1 gets R2=0.
346
+ entire canonical-sorted stream — no grouping by rsg_key, no per-cell
347
+ selection between exact and MA2. Bishop Eq 13.7-13.9 specify j=2,...,J
348
+ with no grouping; only j=1 has no value.
349
349
 
350
350
  Parameters
351
351
  ----------
@@ -354,8 +354,9 @@ def calculate_r2(df: pd.DataFrame, y: str, r2_method: R2Method, n_per_cell: pd.S
354
354
  y : str
355
355
  Name of response variable
356
356
  r2_method : R2Method
357
- 'exact', 'ma2', or 'hybrid' — retained for API compatibility.
358
- Branching is driven by n_per_cell.
357
+ 'exact' or 'ma2', as chosen by SDSRegistry.get_r2_method(). Kept for
358
+ callers and logging; the branch below re-derives it from n_per_cell so
359
+ the two can never disagree.
359
360
  n_per_cell : pd.Series, optional
360
361
  Pre-computed observations per cell. Pass from ADS to avoid recomputation.
361
362
 
@@ -427,7 +428,7 @@ def calculate_vas_residuals(
427
428
  spec : FormulationSpec
428
429
  Analysis specification
429
430
  r2_method : R2Method
430
- 'exact', 'ma2', or 'hybrid' - determines R2 calculation
431
+ 'exact' or 'ma2' - determines R2 calculation
431
432
  n_per_cell : pd.Series, optional
432
433
  Pre-computed observations per cell. Pass from ADS to avoid recomputation.
433
434
  ybar_kt : pd.Series, optional
@@ -39,7 +39,7 @@ SDSReasonType = Literal[
39
39
  ]
40
40
 
41
41
  # R2 calculation method - structure-driven, not SDS-driven
42
- R2Method = Literal['exact', 'ma2', 'hybrid']
42
+ R2Method = Literal['exact', 'ma2']
43
43
 
44
44
 
45
45
  @dataclass(frozen=True)
@@ -116,7 +116,7 @@ class SDSAnalysisPlan:
116
116
  residuals_available : list[str]
117
117
  Which residuals can be calculated
118
118
  residual_calculation_method : str
119
- How R2 is calculated ('exact', 'moving_average', 'hybrid', or 'none')
119
+ How R2 is calculated ('exact', 'ma2', or 'none')
120
120
  main_effects_supported : bool
121
121
  Whether main effect calculations are supported
122
122
  interaction_effects_supported : bool
@@ -158,7 +158,7 @@ class SDSAnalysisPlan:
158
158
  # VAS residual capabilities
159
159
  vas_residuals_supported: bool
160
160
  residuals_available: list[str]
161
- residual_calculation_method: str # 'exact', 'moving_average', 'hybrid', 'none'
161
+ residual_calculation_method: str # 'exact', 'ma2', 'none'
162
162
 
163
163
  # Effects and interactions
164
164
  main_effects_supported: bool
@@ -383,7 +383,7 @@ class SDSRegistry:
383
383
 
384
384
  **SDS 3**: Semi-Complete - Partial replication
385
385
  - Min N_kt = 1 and Max N_kt ≥ 2 (mixed)
386
- - Hybrid variance estimation
386
+ - R2 by moving average across the full sequence (any singleton forces MA2)
387
387
 
388
388
  **Incomplete** (has empty cells, requires sampling plan to detect):
389
389
 
@@ -664,7 +664,7 @@ class SDSRegistry:
664
664
  >>> info['description']
665
665
  'Full replication (all cells n≥2)'
666
666
  >>> info['r2_method']
667
- 'within_cell'
667
+ 'exact'
668
668
  >>> info['capabilities']
669
669
  ['full_vas', 'all_residuals', 'interactions', 'main_effects']
670
670
  """
@@ -680,7 +680,7 @@ class SDSRegistry:
680
680
  1: {
681
681
  'description': 'Full replication (all cells n≥2)',
682
682
  'replication_type': 'full',
683
- 'r2_method': 'within_cell',
683
+ 'r2_method': 'exact',
684
684
  'capabilities': ['full_vas', 'all_residuals', 'interactions', 'main_effects'],
685
685
  'interaction_analysis': True,
686
686
  'variance_decomposition': True,
@@ -688,7 +688,7 @@ class SDSRegistry:
688
688
  2: {
689
689
  'description': 'No replication (all cells n=1)',
690
690
  'replication_type': 'none',
691
- 'r2_method': 'moving_average',
691
+ 'r2_method': 'ma2',
692
692
  'capabilities': ['all_residuals', 'limited_interactions', 'main_effects'],
693
693
  'interaction_analysis': 'limited',
694
694
  'variance_decomposition': True,
@@ -696,7 +696,7 @@ class SDSRegistry:
696
696
  3: {
697
697
  'description': 'Partial replication (mixed n=1 and n≥2)',
698
698
  'replication_type': 'partial',
699
- 'r2_method': 'hybrid',
699
+ 'r2_method': 'ma2', # Any singleton: MA2 over the full sequence (Bishop Eq 13.7-13.9)
700
700
  'capabilities': ['all_residuals', 'partial_interactions', 'main_effects'],
701
701
  'interaction_analysis': 'partial',
702
702
  'variance_decomposition': True,
@@ -720,7 +720,7 @@ class SDSRegistry:
720
720
  6: {
721
721
  'description': 'Incomplete grid with singletons (has 0s, 1s, and ≥2s)',
722
722
  'replication_type': 'partial',
723
- 'r2_method': 'hybrid',
723
+ 'r2_method': 'ma2',
724
724
  'capabilities': ['partial_vas', 'main_effects', 'stratification'],
725
725
  'interaction_analysis': False,
726
726
  'variance_decomposition': True,
@@ -751,23 +751,24 @@ class SDSRegistry:
751
751
  Returns
752
752
  -------
753
753
  R2Method
754
- 'exact' if all cells have replication (n_cell_min >= 2)
755
- 'ma2' if all cells are singletons (n_cell_max == 1)
756
- 'hybrid' for mixed replication (exact where n >= 2, MA2 where n = 1)
754
+ 'exact' if every cell has replication (n_cell_min >= 2)
755
+ 'ma2' if any cell is a singleton (n_cell_min == 1), whether all
756
+ cells are singletons (ADS 2) or only some (ADS 3)
757
757
 
758
758
  Notes
759
759
  -----
760
760
  Deterministic rule based on Bishop methodology:
761
- - Eq 59 (exact): R2 = Y - Ȳ_kt, requires replication
762
- - Eq 66 (MA2): R2 = (Y_j - Y_{j-1}) / 2, for singletons
763
- - Hybrid: exact where replicated, MA2 where singleton
761
+ - Eq 59 (exact): R2 = Y - Ȳ_kt, requires replication in every cell
762
+ - Eq 13.7-13.9 (MA2): R2 = (Y_j - Y_{j-1}) / 2 over the whole ordered
763
+ sequence, j = 2..J, with no grouping by cell. Once any cell is a
764
+ singleton this applies to every observation; the replicated cells'
765
+ within-cell deviations are not used for R2. There is no per-cell
766
+ mixing of the two (the earlier "hybrid" was removed in cb037f8, and
767
+ the ADS 3 reference assertions validate the ungrouped MA2).
764
768
  """
765
769
  if stats.n_cell_min >= 2:
766
770
  return 'exact'
767
- elif stats.n_cell_max == 1:
768
- return 'ma2'
769
- else:
770
- return 'hybrid'
771
+ return 'ma2'
771
772
 
772
773
  def validate_sds_for_analysis(self, sds: int, analysis_type: str) -> bool:
773
774
  """
@@ -1296,7 +1297,7 @@ class SDSRegistry:
1296
1297
  invalid_charts=[],
1297
1298
  vas_residuals_supported=True,
1298
1299
  residuals_available=['R1', 'R2', 'R3', 'R4', 'R5', 'R6'],
1299
- residual_calculation_method='moving_average',
1300
+ residual_calculation_method='ma2',
1300
1301
  main_effects_supported=True,
1301
1302
  interaction_effects_supported=True,
1302
1303
  supports_stratification=True,
@@ -1324,7 +1325,7 @@ class SDSRegistry:
1324
1325
  invalid_charts=[],
1325
1326
  vas_residuals_supported=True,
1326
1327
  residuals_available=['R1', 'R2', 'R3', 'R4', 'R5', 'R6'],
1327
- residual_calculation_method='hybrid',
1328
+ residual_calculation_method='ma2',
1328
1329
  main_effects_supported=True,
1329
1330
  interaction_effects_supported=True,
1330
1331
  supports_stratification=True,
@@ -1335,7 +1336,8 @@ class SDSRegistry:
1335
1336
  'Pilot studies with targeted replication',
1336
1337
  ],
1337
1338
  limitations=[
1338
- 'R2 uses hybrid calculation (exact where possible, approximate elsewhere)',
1339
+ 'R2 uses the 2-point moving average over the full sequence (Bishop Eq 13.7-13.9); '
1340
+ 'the replicated cells contribute no separate within-cell estimate',
1339
1341
  'Variance estimates less precise than SDS 1',
1340
1342
  'May have unequal subgroup sizes',
1341
1343
  ],
@@ -0,0 +1,105 @@
1
+ """
2
+ Series-length precision of the sigma estimate.
3
+
4
+ One fact for the design report: where the sigma behind the natural process
5
+ limits comes from at the observed structure and series length. No threshold,
6
+ label, or warning is attached; the analyst weighs it (Bishop: judgment belongs
7
+ to the analyst, not to a rule).
8
+
9
+ Two sources of sigma:
10
+
11
+ - Every cell replicated (ADS 1): within-cell deviation. The number of time
12
+ periods does not enter; T = 1 is a complete study.
13
+ - Any singleton cell (ADS 2, ADS 3): the 2-point moving range over the ordered
14
+ sequence, T - 1 ranges for T time points.
15
+
16
+ The sentence names the source and the count. The within-cell degrees of freedom
17
+ and the 80% interval of the moving-range estimate at this T
18
+ (``MR_SIGMA_INTERVAL_80``, from #114) ride on the result for callers who want
19
+ them; the report does not print them.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+
26
+ from .spc_constants import MR_SIGMA_INTERVAL_80
27
+
28
+ _TABLE_MAX = max(MR_SIGMA_INTERVAL_80)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class SeriesLengthPrecision:
33
+ """
34
+ Where sigma comes from, and how precise it is, at the observed series length.
35
+
36
+ Attributes
37
+ ----------
38
+ T : int or None
39
+ Distinct time points on the analysis dataset; None when no time variable.
40
+ n_moving_ranges : int or None
41
+ T - 1 when the moving range carries sigma and T >= 2; else None.
42
+ within_cell_df : int
43
+ Sum over cells of (N_kt - 1); 0 when no cell is replicated.
44
+ sigma_from_time : bool
45
+ True when sigma rests on moving ranges (any singleton cell); False when
46
+ it rests on within-cell replication.
47
+ mr_interval_80 : tuple of (float, float) or None
48
+ (p10, p90) of the moving-range sigma estimate relative to the truth on a
49
+ stable process, from ``MR_SIGMA_INTERVAL_80``; only when
50
+ ``sigma_from_time`` and 3 <= T <= 30.
51
+ description : str
52
+ The sentence printed in the design report, e.g.
53
+ ``T=4. Sigma for X/mR rests on 3 moving ranges.`` Empty when there is
54
+ nothing to say (no time variable and no replication).
55
+ """
56
+
57
+ T: int | None
58
+ n_moving_ranges: int | None
59
+ within_cell_df: int
60
+ sigma_from_time: bool
61
+ mr_interval_80: tuple[float, float] | None
62
+ description: str
63
+
64
+
65
+ def assess_series_length(T: int | None, within_cell_df: int, sigma_from_time: bool) -> SeriesLengthPrecision:
66
+ """
67
+ Build the precision statement for a study.
68
+
69
+ The sentence names only the source of sigma at this structure and T. The
70
+ numbers behind it (``within_cell_df``, ``mr_interval_80``) ride on the
71
+ result for callers who want them; the report does not print them.
72
+
73
+ Parameters
74
+ ----------
75
+ T : int or None
76
+ Distinct time points on the analysis dataset (None if no time variable).
77
+ within_cell_df : int
78
+ Sum over cells of (N_kt - 1) on the analysis dataset.
79
+ sigma_from_time : bool
80
+ True when the limits rest on the moving range (any singleton cell).
81
+
82
+ Returns
83
+ -------
84
+ SeriesLengthPrecision
85
+ """
86
+ if not sigma_from_time:
87
+ if T is None:
88
+ description = 'Sigma rests on within-cell replication.'
89
+ else:
90
+ description = f'T={T}. Sigma rests on within-cell replication.'
91
+ return SeriesLengthPrecision(T, None, within_cell_df, False, None, description)
92
+
93
+ if T is None:
94
+ return SeriesLengthPrecision(None, None, within_cell_df, True, None, '')
95
+
96
+ n_mr = T - 1 if T >= 2 else 0
97
+ interval = MR_SIGMA_INTERVAL_80.get(T) if T <= _TABLE_MAX else None
98
+
99
+ if T < 2:
100
+ description = f'T={T}. Sigma for X/mR rests on the moving range, which needs at least 2 time points.'
101
+ elif T == 2:
102
+ description = 'T=2. Sigma for X/mR rests on a single moving range.'
103
+ else:
104
+ description = f'T={T}. Sigma for X/mR rests on {n_mr} moving ranges.'
105
+ return SeriesLengthPrecision(T, n_mr, within_cell_df, True, interval, description)
@@ -690,3 +690,47 @@ RESIDUAL_ALIASES: dict[str, str] = {
690
690
  'design_condition_main_effects': 'R5',
691
691
  'design_factor_main_effects': 'R6',
692
692
  }
693
+
694
+
695
+ # ============================================================================
696
+ # Series-length precision of the moving-range sigma estimate
697
+ # ============================================================================
698
+
699
+ # 80% interval (p10, p90) of MRbar/d2 relative to the true sigma, for a stable
700
+ # normal process observed at n points, n = 3..30. Generated by
701
+ # validation/short_series_bands.py (seed 20260903, 100,000 replicates, an
702
+ # independent stream per n), contributed by @rabujamra in #114 / PR #119. Read
703
+ # as: at n=4, 80% of moving-range sigma estimates fall between 0.43x and 1.68x
704
+ # the truth. The spread narrows monotonically with n, so the n=30 row bounds
705
+ # every longer series. Stated as a fact in the design report; the library
706
+ # attaches no threshold, label, or warning to it.
707
+ MR_SIGMA_INTERVAL_80: dict[int, tuple[float, float]] = {
708
+ 3: (0.337, 1.810),
709
+ 4: (0.430, 1.676),
710
+ 5: (0.488, 1.593),
711
+ 6: (0.528, 1.526),
712
+ 7: (0.565, 1.484),
713
+ 8: (0.595, 1.453),
714
+ 9: (0.618, 1.418),
715
+ 10: (0.637, 1.396),
716
+ 11: (0.656, 1.375),
717
+ 12: (0.670, 1.360),
718
+ 13: (0.680, 1.344),
719
+ 14: (0.694, 1.330),
720
+ 15: (0.703, 1.315),
721
+ 16: (0.714, 1.310),
722
+ 17: (0.722, 1.300),
723
+ 18: (0.730, 1.288),
724
+ 19: (0.739, 1.280),
725
+ 20: (0.745, 1.274),
726
+ 21: (0.749, 1.266),
727
+ 22: (0.756, 1.258),
728
+ 23: (0.761, 1.253),
729
+ 24: (0.766, 1.249),
730
+ 25: (0.771, 1.243),
731
+ 26: (0.775, 1.238),
732
+ 27: (0.780, 1.234),
733
+ 28: (0.784, 1.229),
734
+ 29: (0.787, 1.224),
735
+ 30: (0.790, 1.220),
736
+ }
@@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, Literal
29
29
  from .exceptions import ChartNotAvailableError, FactorNotFoundError, ValidationError
30
30
  from .residual_calculator import resolve_r6_groupby
31
31
  from .sds_detector import SDSRegistry
32
+ from .series_length import SeriesLengthPrecision, assess_series_length
32
33
  from .spc_constants import (
33
34
  ALL_RESIDUALS,
34
35
  RESIDUAL_ALIASES,
@@ -173,6 +174,7 @@ class DesignReport:
173
174
  _pds_result: SDSResult | None = None # Plan Design State
174
175
  _ods_result: SDSResult | None = None # Observed Design State
175
176
  _ads_result: SDSResult | None = None # Analytical Design State
177
+ _series_length: SeriesLengthPrecision | None = None # Where sigma comes from, and how precise
176
178
 
177
179
  @property
178
180
  def factors_table(self) -> pd.DataFrame:
@@ -765,6 +767,12 @@ class DesignReport:
765
767
  lines.append('')
766
768
  lines.append(f' Structure: {self.structure_summary}')
767
769
 
770
+ # Series length: where sigma comes from at this structure and T. A fact beside the
771
+ # design state, not a verdict on it — ADS 2 is ADS 2 whether T is 4 or 400 (#114).
772
+ # No threshold, label, or warning is attached.
773
+ if self._series_length is not None and self._series_length.description:
774
+ lines.append(f' Series length: {self._series_length.description}')
775
+
768
776
  # Available analyses (derived from ADS)
769
777
  if self._ads_result and self._ads_result.sds > 0:
770
778
  lines.extend(self._repr_available_analyses())
@@ -1111,6 +1119,39 @@ class Study:
1111
1119
  """
1112
1120
  return self.analytical_design_state.reason
1113
1121
 
1122
+ @property
1123
+ def series_length(self) -> SeriesLengthPrecision:
1124
+ """
1125
+ Where sigma comes from at this structure and series length.
1126
+
1127
+ A fact stated beside the design state, not a judgment on it: no
1128
+ threshold, label, or warning. With any singleton cell the limits rest on
1129
+ T - 1 moving ranges; with every cell replicated they rest on within-cell
1130
+ replication and T does not enter. The result also carries
1131
+ ``within_cell_df`` and, for the moving-range case, ``mr_interval_80``
1132
+ (from ``MR_SIGMA_INTERVAL_80``) for callers who want the numbers; the
1133
+ report prints only the sentence.
1134
+
1135
+ Returns
1136
+ -------
1137
+ SeriesLengthPrecision
1138
+ """
1139
+ ads_df = self._ads.analysis_dataset
1140
+ T = None
1141
+ if self._spec.time_var and self._spec.time_var in ads_df.columns:
1142
+ T = int(ads_df[self._spec.time_var].nunique())
1143
+ within_cell_df = 0
1144
+ if 'cell_key' in ads_df.columns and len(ads_df):
1145
+ sizes = ads_df.groupby('cell_key', observed=True).size()
1146
+ within_cell_df = int((sizes - 1).sum())
1147
+ sigma_from_time = self.analytical_design_state.sds != 1
1148
+ return assess_series_length(T, within_cell_df, sigma_from_time)
1149
+
1150
+ @property
1151
+ def series_length_description(self) -> str:
1152
+ """The series-length sentence printed in the design report (may be empty)."""
1153
+ return self.series_length.description
1154
+
1114
1155
  @property
1115
1156
  def ads_description(self) -> str:
1116
1157
  """
@@ -1667,6 +1708,7 @@ class Study:
1667
1708
  _pds_result=self._pds_result,
1668
1709
  _ods_result=self._sds_result,
1669
1710
  _ads_result=self._ads.analytical_design_state,
1711
+ _series_length=self.series_length,
1670
1712
  )
1671
1713
 
1672
1714
  def capability(
@@ -44,7 +44,7 @@ dependencies = [
44
44
  "numpy>=1.23",
45
45
  "pandas>=2.0,<3",
46
46
  "natsort>=8.0.0",
47
- "plotly>=5.18,<7",
47
+ "plotly>=5.18,<8",
48
48
  # NotRequired (used in processbehavior.types) is only in typing from 3.11;
49
49
  # on 3.10 it comes from typing_extensions.
50
50
  "typing_extensions>=4.0; python_version < '3.11'",