process-improve 1.97.1__tar.gz → 1.97.2__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 (238) hide show
  1. {process_improve-1.97.1 → process_improve-1.97.2}/PKG-INFO +2 -2
  2. {process_improve-1.97.1 → process_improve-1.97.2}/pyproject.toml +3 -3
  3. {process_improve-1.97.1 → process_improve-1.97.2}/pyproject.toml.orig +3 -3
  4. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/_extras.py +34 -18
  5. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs.py +4 -2
  6. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_response_surface.py +6 -2
  7. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/monitoring/control_charts.py +214 -77
  8. {process_improve-1.97.1 → process_improve-1.97.2}/README.md +0 -0
  9. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/__init__.py +0 -0
  10. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/_linalg.py +0 -0
  11. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/_random.py +0 -0
  12. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/_remote_data.py +0 -0
  13. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/__init__.py +0 -0
  14. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_batch_monitor.py +0 -0
  15. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_batch_pca.py +0 -0
  16. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_batch_plots.py +0 -0
  17. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_batch_pls.py +0 -0
  18. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_common.py +0 -0
  19. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_gaps.py +0 -0
  20. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/_online.py +0 -0
  21. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/alignment_helpers.py +0 -0
  22. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/control.py +0 -0
  23. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/data_input.py +0 -0
  24. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/datasets.py +0 -0
  25. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/features.py +0 -0
  26. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/plotting.py +0 -0
  27. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/preprocessing.py +0 -0
  28. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/batch/tools.py +0 -0
  29. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/bivariate/__init__.py +0 -0
  30. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/bivariate/_elbow_peak.py +0 -0
  31. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/bivariate/methods.py +0 -0
  32. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/bivariate/tools.py +0 -0
  33. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/chemistry/__init__.py +0 -0
  34. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/chemistry/preprocessing.py +0 -0
  35. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/config.py +0 -0
  36. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/batch/batch-fake-data.csv +0 -0
  37. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/batch/details.txt +0 -0
  38. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/batch/dryer.csv +0 -0
  39. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/batch/nylon.csv +0 -0
  40. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/experiments/boilingpot.csv +0 -0
  41. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/experiments/golf.csv +0 -0
  42. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/experiments/pollutant.csv +0 -0
  43. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/experiments/solar.csv +0 -0
  44. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/experiments/test_doe1.csv +0 -0
  45. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/monitoring/batch-yield-and-purity.csv +0 -0
  46. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/monitoring/rubber-colour.csv +0 -0
  47. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/C.csv +0 -0
  48. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/Hotellings_T2_A3.csv +0 -0
  49. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/Hotellings_T2_A6.csv +0 -0
  50. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/LDPE.csv +0 -0
  51. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/P.csv +0 -0
  52. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/R.csv +0 -0
  53. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/T.csv +0 -0
  54. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/U.csv +0 -0
  55. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/W.csv +0 -0
  56. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/LDPE/Yhat_A6.csv +0 -0
  57. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/cheddar-cheese.csv +0 -0
  58. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/NOTICE +0 -0
  59. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/extraction1.csv +0 -0
  60. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/extraction2.csv +0 -0
  61. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/extraction3.csv +0 -0
  62. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/ftir1.csv +0 -0
  63. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/ftir2.csv +0 -0
  64. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/dtu-pectin/ftir3.csv +0 -0
  65. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/kamyr.csv +0 -0
  66. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/solvents.csv +0 -0
  67. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tablet-spectra.csv +0 -0
  68. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/SOURCE +0 -0
  69. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group1.csv +0 -0
  70. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group2.csv +0 -0
  71. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group3.csv +0 -0
  72. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group4.csv +0 -0
  73. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group5.csv +0 -0
  74. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/process_conditions.csv +0 -0
  75. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group1.csv +0 -0
  76. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group2.csv +0 -0
  77. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group3.csv +0 -0
  78. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group4.csv +0 -0
  79. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group5.csv +0 -0
  80. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/datasets/multivariate/tpls-pyphi/quality_indicators.csv +0 -0
  81. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/__init__.py +0 -0
  82. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/__init__.py +0 -0
  83. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/_shared.py +0 -0
  84. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/aliasing.py +0 -0
  85. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/box_cox.py +0 -0
  86. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/curvature.py +0 -0
  87. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/diagnostics.py +0 -0
  88. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/lack_of_fit.py +0 -0
  89. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/lenth.py +0 -0
  90. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/mixture.py +0 -0
  91. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/model_selection.py +0 -0
  92. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/ols_extractors.py +0 -0
  93. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/prediction.py +0 -0
  94. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_analyses/transforms.py +0 -0
  95. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_blocking.py +0 -0
  96. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_desirability.py +0 -0
  97. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_finite_fields.py +0 -0
  98. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_lm.py +0 -0
  99. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_moment_aberration.py +0 -0
  100. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/__init__.py +0 -0
  101. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/analyze_experiment.py +0 -0
  102. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/augment_design.py +0 -0
  103. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/create_factorial_design.py +0 -0
  104. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/doe_knowledge.py +0 -0
  105. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/evaluate_design.py +0 -0
  106. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/fit_linear_model.py +0 -0
  107. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/generate_design.py +0 -0
  108. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/optimize_responses.py +0 -0
  109. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/recommend_strategy.py +0 -0
  110. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/trade_off_table.py +0 -0
  111. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_tools/visualize_doe.py +0 -0
  112. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/_uniform_sampling.py +0 -0
  113. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/analysis.py +0 -0
  114. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/augment.py +0 -0
  115. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/datasets.py +0 -0
  116. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_constrained.py +0 -0
  117. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_factorial.py +0 -0
  118. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_mixture.py +0 -0
  119. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_mixture_constrained.py +0 -0
  120. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_omars.py +0 -0
  121. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_omars_ilp.py +0 -0
  122. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_optimal.py +0 -0
  123. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_screening.py +0 -0
  124. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_space_filling.py +0 -0
  125. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_supersaturated.py +0 -0
  126. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/designs_utils.py +0 -0
  127. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/evaluate.py +0 -0
  128. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/factor.py +0 -0
  129. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/__init__.py +0 -0
  130. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/api.py +0 -0
  131. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/data/concepts.yaml +0 -0
  132. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/data/decision_rules.yaml +0 -0
  133. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/data/design_types.yaml +0 -0
  134. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/data/diagnostics.yaml +0 -0
  135. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/engine.py +0 -0
  136. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/knowledge/models.py +0 -0
  137. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/models.py +0 -0
  138. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/omars.py +0 -0
  139. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/omars_trade_off.py +0 -0
  140. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/optimal.py +0 -0
  141. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/optimization.py +0 -0
  142. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/region.py +0 -0
  143. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/simulations.py +0 -0
  144. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/strategy/__init__.py +0 -0
  145. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/strategy/budget.py +0 -0
  146. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/strategy/domain_templates.py +0 -0
  147. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/strategy/engine.py +0 -0
  148. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/strategy/models.py +0 -0
  149. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/structures.py +0 -0
  150. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/tools.py +0 -0
  151. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/trade_off.py +0 -0
  152. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/__init__.py +0 -0
  153. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/api.py +0 -0
  154. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/__init__.py +0 -0
  155. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/cube_plot.py +0 -0
  156. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/design_quality.py +0 -0
  157. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/diagnostics.py +0 -0
  158. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/effects.py +0 -0
  159. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/optimization_plots.py +0 -0
  160. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/registry.py +0 -0
  161. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/significance.py +0 -0
  162. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/square_plot.py +0 -0
  163. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/experiments/visualization/plots/surfaces.py +0 -0
  164. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/interactions.py +0 -0
  165. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/mcp_server.py +0 -0
  166. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/monitoring/__init__.py +0 -0
  167. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/monitoring/metrics.py +0 -0
  168. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/monitoring/tools.py +0 -0
  169. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/__init__.py +0 -0
  170. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_adaptive.py +0 -0
  171. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_asca.py +0 -0
  172. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_base.py +0 -0
  173. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_categorical.py +0 -0
  174. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_common.py +0 -0
  175. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_cv_criteria.py +0 -0
  176. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_diagnostics.py +0 -0
  177. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_impute.py +0 -0
  178. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_limits.py +0 -0
  179. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_mbpca.py +0 -0
  180. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_mbpls.py +0 -0
  181. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_nipals.py +0 -0
  182. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_null.py +0 -0
  183. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_opls.py +0 -0
  184. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_pca.py +0 -0
  185. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_pca_pls.py +0 -0
  186. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_pls.py +0 -0
  187. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_plsda.py +0 -0
  188. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_preprocessing.py +0 -0
  189. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_prm.py +0 -0
  190. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_procrustes.py +0 -0
  191. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_projection.py +0 -0
  192. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_resampling.py +0 -0
  193. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_robust.py +0 -0
  194. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/_tpls.py +0 -0
  195. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/methods.py +0 -0
  196. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/plots.py +0 -0
  197. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/multivariate/tools.py +0 -0
  198. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/py.typed +0 -0
  199. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/recipes.py +0 -0
  200. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/regression/__init__.py +0 -0
  201. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/regression/_robust_regression.py +0 -0
  202. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/regression/methods.py +0 -0
  203. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/regression/tools.py +0 -0
  204. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/__init__.py +0 -0
  205. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/analysis.py +0 -0
  206. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/designed.py +0 -0
  207. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/diagnostics.py +0 -0
  208. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/ingest.py +0 -0
  209. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/mam.py +0 -0
  210. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/panel.py +0 -0
  211. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/recipes.py +0 -0
  212. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/tools.py +0 -0
  213. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/sensory/validation.py +0 -0
  214. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/__init__.py +0 -0
  215. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/batch.py +0 -0
  216. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/context.py +0 -0
  217. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/latent.py +0 -0
  218. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/model.py +0 -0
  219. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/recipes.py +0 -0
  220. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/simulation/tools.py +0 -0
  221. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/tool_safety.py +0 -0
  222. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/tool_spec.py +0 -0
  223. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/univariate/__init__.py +0 -0
  224. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/univariate/metrics.py +0 -0
  225. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/univariate/tools.py +0 -0
  226. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/__init__.py +0 -0
  227. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/adapters/__init__.py +0 -0
  228. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/adapters/base.py +0 -0
  229. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/adapters/echarts_adapter.py +0 -0
  230. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/adapters/plotly_adapter.py +0 -0
  231. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/charts/__init__.py +0 -0
  232. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/charts/boxplot.py +0 -0
  233. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/colors.py +0 -0
  234. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/raincloud.py +0 -0
  235. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/spec.py +0 -0
  236. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/themes.py +0 -0
  237. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/tools.py +0 -0
  238. {process_improve-1.97.1 → process_improve-1.97.2}/src/process_improve/visualization/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-improve
3
- Version: 1.97.1
3
+ Version: 1.97.2
4
4
  Summary: Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.
5
5
  Keywords: Designed Experiments,Latent Variables,PCA,PLS,Multivariate Data Analysis,Batch data analysis
6
6
  Author: Kevin Dunn
@@ -42,7 +42,7 @@ Requires-Dist: osqp>=1.0 ; extra == 'control'
42
42
  Requires-Dist: coverage>=7.13.1 ; extra == 'dev'
43
43
  Requires-Dist: hypothesis>=6.100 ; extra == 'dev'
44
44
  Requires-Dist: matplotlib-stubs>=0.3.11 ; extra == 'dev'
45
- Requires-Dist: mypy>=1.19.1,<2.4 ; extra == 'dev'
45
+ Requires-Dist: mypy>=1.19.1,<2.5 ; extra == 'dev'
46
46
  Requires-Dist: pandas-stubs>=2.3.3.260113 ; extra == 'dev'
47
47
  Requires-Dist: plotly-stubs>=0.0.6 ; extra == 'dev'
48
48
  Requires-Dist: pre-commit>=4.5.1 ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "process-improve"
3
- version = "1.97.1"
3
+ version = "1.97.2"
4
4
  description = "Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -74,7 +74,7 @@ dev = [
74
74
  "coverage>=7.13.1",
75
75
  "hypothesis>=6.100",
76
76
  "matplotlib-stubs>=0.3.11",
77
- "mypy>=1.19.1,<2.4",
77
+ "mypy>=1.19.1,<2.5",
78
78
  "pandas-stubs>=2.3.3.260113",
79
79
  "plotly-stubs>=0.0.6",
80
80
  "pre-commit>=4.5.1",
@@ -108,7 +108,7 @@ dev = [
108
108
  "coverage>=7.13.1",
109
109
  "hypothesis>=6.100",
110
110
  "matplotlib-stubs>=0.3.11",
111
- "mypy>=1.19.1,<2.4",
111
+ "mypy>=1.19.1,<2.5",
112
112
  "pandas-stubs>=2.3.3.260113",
113
113
  "plotly-stubs>=0.0.6",
114
114
  "pre-commit>=4.5.1",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "process-improve"
3
- version = "1.97.1"
3
+ version = "1.97.2"
4
4
  description = 'Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.'
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -122,7 +122,7 @@ dev = [
122
122
  "coverage>=7.13.1",
123
123
  "hypothesis>=6.100",
124
124
  "matplotlib-stubs>=0.3.11",
125
- "mypy>=1.19.1,<2.4",
125
+ "mypy>=1.19.1,<2.5",
126
126
  "pandas-stubs>=2.3.3.260113",
127
127
  "plotly-stubs>=0.0.6",
128
128
  "pre-commit>=4.5.1",
@@ -157,7 +157,7 @@ dev = [
157
157
  "coverage>=7.13.1",
158
158
  "hypothesis>=6.100",
159
159
  "matplotlib-stubs>=0.3.11",
160
- "mypy>=1.19.1,<2.4",
160
+ "mypy>=1.19.1,<2.5",
161
161
  "pandas-stubs>=2.3.3.260113",
162
162
  "plotly-stubs>=0.0.6",
163
163
  "pre-commit>=4.5.1",
@@ -58,11 +58,16 @@ def require_extra(missing: str, extra: str) -> ImportError:
58
58
  class _MissingExtra:
59
59
  """Stand-in for an optional module that was not installed.
60
60
 
61
- Attribute access raises :class:`AttributeError` (Python's special-method
62
- convention -- ``hasattr(stub, 'Figure')`` returns ``False``) with the
63
- canonical "install the extra" remediation message in the body. Direct
64
- *calls* on the stub raise :class:`ImportError` because that is the
65
- natural error class for "you need to install this dependency".
61
+ Using the stub in any way that needs the real package raises
62
+ :class:`ImportError` with the canonical "install the extra" message, so a
63
+ missing ``plotting`` extra fails the same way as a missing ``expt`` extra.
64
+ Attribute access returns a child stub that remembers the dotted path, so
65
+ ``go.Figure(...)`` and ``go.layout.Template(...)`` raise ``ImportError`` at
66
+ the call, and indexing (``pio.templates[name]``) raises it too.
67
+
68
+ Dunder lookups (``__array__``, ``__deepcopy__``, ...) raise
69
+ :class:`AttributeError` instead, so protocol probes such as ``copy``,
70
+ ``pickle`` and numpy see a missing attribute, per the Python data model.
66
71
 
67
72
  Lets a module top-level write::
68
73
 
@@ -75,21 +80,32 @@ class _MissingExtra:
75
80
  who never touch the optional surface.
76
81
  """
77
82
 
78
- __slots__ = ("_extra", "_missing")
83
+ __slots__ = ("_extra", "_missing", "_path")
79
84
 
80
- def __init__(self, missing: str, extra: str) -> None:
85
+ def __init__(self, missing: str, extra: str, path: str = "") -> None:
81
86
  self._missing = missing
82
87
  self._extra = extra
83
-
84
- def __getattr__(self, name: str) -> object:
85
- # __getattr__ must raise AttributeError so ``hasattr`` and friends
86
- # behave per the Python data model (CodeQL py/non-standard-exception
87
- # -raised-in-special-method). The install-hint message is preserved
88
- # in the exception text.
89
- raise AttributeError(f"{_extra_message(self._missing, self._extra)}\n(Attempted attribute access: {name!r}.)")
88
+ self._path = path
89
+
90
+ def _error(self) -> ImportError:
91
+ hint = f"\n(Attempted to use {self._path!r}.)" if self._path else ""
92
+ return ImportError(f"{_extra_message(self._missing, self._extra)}{hint}")
93
+
94
+ def __getattr__(self, name: str) -> _MissingExtra:
95
+ if name.startswith("__") and name.endswith("__"):
96
+ # __getattr__ must raise AttributeError for protocol lookups so
97
+ # ``hasattr`` and friends behave per the data model (CodeQL
98
+ # py/non-standard-exception-raised-in-special-method).
99
+ raise AttributeError(
100
+ f"{_extra_message(self._missing, self._extra)}\n(Attempted attribute access: {name!r}.)"
101
+ )
102
+ return _MissingExtra(self._missing, self._extra, f"{self._path}.{name}" if self._path else name)
90
103
 
91
104
  def __call__(self, *_args: object, **_kwargs: object) -> object:
92
- # __call__ has no equivalent convention; raise ImportError here
93
- # because callers that try to use the stub directly are asking
94
- # for the missing package.
95
- raise require_extra(self._missing, self._extra)
105
+ raise self._error()
106
+
107
+ def __getitem__(self, _key: object) -> object:
108
+ raise self._error()
109
+
110
+ def __setitem__(self, _key: object, _value: object) -> None:
111
+ raise self._error()
@@ -678,7 +678,8 @@ def generate_design( # noqa: PLR0913
678
678
  ``Constraint(expression="3*T + 5*D <= 600")``; a dict of ``Constraint`` fields or
679
679
  a bare expression string is accepted too. Honoured by the optimal
680
680
  families (``"d_optimal"``, chosen automatically when constraints are given,
681
- ``"i_optimal"``, ``"a_optimal"`` and ``"e_optimal"``), whose runs are selected
681
+ ``"i_optimal"``, ``"a_optimal"``, ``"e_optimal"``, ``"g_optimal"`` and
682
+ ``"k_optimal"``), whose runs are selected
682
683
  from a candidate set of feasible points; by ``"mixture"`` (linear constraints
683
684
  in the proportions); and by the ``"sobol"``, ``"halton"``, ``"maximin"`` and
684
685
  ``"maxpro"`` space-filling designs. Other design types do not enforce them and set
@@ -695,7 +696,8 @@ def generate_design( # noqa: PLR0913
695
696
  Ignored by the classical (non-optimal) design families.
696
697
  fixed_runs : pandas.DataFrame or None
697
698
  Runs to hold fixed while the optimizer fills the rest (design augmentation), for the
698
- optimal families only (``"d_optimal"``, ``"i_optimal"``, ``"a_optimal"``, ``"e_optimal"``). One row per
699
+ optimal families only (``"d_optimal"``, ``"i_optimal"``, ``"a_optimal"``, ``"e_optimal"``,
700
+ ``"g_optimal"``, ``"k_optimal"``). One row per
699
701
  fixed run, one column per factor, in the same coding as the returned design: continuous
700
702
  factors in coded ``[-1, 1]`` units, categorical factors as level labels.
701
703
  The fixed runs occupy the first rows of the result and ``budget`` counts them, so
@@ -456,8 +456,12 @@ def dispatch_dsd(factors: list[Factor], budget: int | None = None) -> tuple[np.n
456
456
  two zeros (one in each half of the foldover) become ``+z`` and ``-z``, and the
457
457
  single centre run becomes two, with the categorical factors at ``+b`` and ``-b``.
458
458
  The signs are chosen to maximise the determinant of the main-effects information
459
- matrix. Every main effect stays orthogonal to every second-order effect; the
460
- categorical main effects are slightly correlated with each other.
459
+ matrix. Every main effect stays orthogonal to every second-order effect, and the
460
+ continuous main effects stay orthogonal to each other. Resolving the zeros costs
461
+ some orthogonality between a categorical main effect and the other main effects:
462
+ it is slightly correlated with each continuous main effect (``|r|`` about 0.17 for
463
+ four continuous factors and one categorical factor in 14 runs) and with the other
464
+ categorical main effects.
461
465
 
462
466
  A ``budget`` larger than the minimal design adds fake factors: the conference
463
467
  matrix of the largest buildable order whose design fits the budget is used, and its
@@ -1,7 +1,8 @@
1
1
  """Class for ControlChart: robust control charts with a balance between CUSUM and Shewhart properties."""
2
2
 
3
3
  import logging
4
- from typing import ClassVar
4
+ from collections.abc import Mapping
5
+ from typing import Any, ClassVar
5
6
 
6
7
  import numpy as np
7
8
  import pandas as pd
@@ -18,6 +19,15 @@ logger = logging.getLogger(__name__)
18
19
  #: (scipy.integrate.quad of rho_norm(z) * phi(z); E = 0.3061160, so
19
20
  #: c_k = 3.266736). This makes E[rho(Z)] = 1, the condition for the scale
20
21
  #: estimates built from rho to be consistent for sigma on Gaussian data.
22
+ #:
23
+ #: Departure from the paper, kept deliberately: equation (13) of Gelper, Fried and
24
+ #: Croux (2010, p 289) pairs the cutoff k = 2 with c_k = 2.52 ("for the common choice
25
+ #: of k = 2 we have c_k = 2.52"; numerically 1 / E[rho_norm(Z)] = 2.5153 for k = 2).
26
+ #: This module uses 2.52 as the cutoff instead, with the matching c_k above. Both pairs
27
+ #: give consistent scale estimates on Gaussian data, but this biweight saturates later
28
+ #: (at ``|x| > 2.52`` rather than ``|x| > 2``), so it down-weights large forecast errors
29
+ #: less than the paper's. The constants are left unchanged because existing results
30
+ #: and callers depend on them.
21
31
  BIWEIGHT_RHO_CONSISTENCY = 3.266736
22
32
 
23
33
 
@@ -38,8 +48,9 @@ def rho(x: float, k: float = 2.52) -> float:
38
48
  x : float
39
49
  Value at which to evaluate the bi-weight rho function.
40
50
  k : float, optional
41
- Bi-weight cutoff. The default of 2.52 is from p 289 of the referenced
42
- paper.
51
+ Bi-weight cutoff. The default of 2.52 is kept for compatibility. In the
52
+ paper (equation 13, p 289) 2.52 is instead the consistency constant c_k
53
+ for the cutoff k = 2; see the note on ``BIWEIGHT_RHO_CONSISTENCY``.
43
54
 
44
55
  Returns
45
56
  -------
@@ -55,12 +66,40 @@ def rho(x: float, k: float = 2.52) -> float:
55
66
  return c_k if np.abs(x) > k else c_k * (1 - np.power(1 - np.power(x / k, 2), 3))
56
67
 
57
68
 
69
+ def _rho_array(x: np.ndarray | pd.Series, k: float = 2.52) -> np.ndarray:
70
+ """
71
+ Evaluate the bi-weight :func:`rho` element-wise over an array.
72
+
73
+ Replaces ``np.vectorize(rho)``, which calls the scalar function once per element. The
74
+ polynomial branch is evaluated on ``x`` clipped to ``[-k, k]``: ``np.where`` evaluates both
75
+ branches, and an unclipped huge ``|x|`` would overflow in ``np.power`` and raise a
76
+ ``RuntimeWarning`` from values that are then discarded. Saturated entries take ``c_k``
77
+ directly and NaN propagates, exactly as in :func:`rho`.
78
+
79
+ Parameters
80
+ ----------
81
+ x : np.ndarray or pd.Series
82
+ Values at which to evaluate the bi-weight rho function.
83
+ k : float, optional
84
+ Bi-weight cutoff, as in :func:`rho`.
85
+
86
+ Returns
87
+ -------
88
+ np.ndarray
89
+ ``rho(x)`` for every element of ``x``, as float64.
90
+ """
91
+ x = np.asarray(x, dtype=float)
92
+ c_k = BIWEIGHT_RHO_CONSISTENCY
93
+ inside = c_k * (1 - np.power(1 - np.power(np.clip(x, -k, k) / k, 2), 3))
94
+ return np.where(np.abs(x) > k, c_k, inside)
95
+
96
+
58
97
  def _finite(values: pd.Series | np.ndarray) -> bool:
59
98
  """Return ``True`` if at least one entry of ``values`` is finite."""
60
99
  return bool(np.isfinite(np.asarray(values, dtype=float)).any())
61
100
 
62
101
 
63
- def _training_error_radicand(future_errors: pd.Series, rho_func: np.vectorize) -> float:
102
+ def _training_error_radicand(future_errors: pd.Series) -> float:
64
103
  """
65
104
  Return tau squared for the training-sample errors, or NaN when it is undefined.
66
105
 
@@ -74,8 +113,6 @@ def _training_error_radicand(future_errors: pd.Series, rho_func: np.vectorize) -
74
113
  ----------
75
114
  future_errors : pd.Series
76
115
  One-step-ahead errors over the training samples (those after the warm-up).
77
- rho_func : np.vectorize
78
- Element-wise bounded loss applied to the standardised errors.
79
116
 
80
117
  Returns
81
118
  -------
@@ -86,14 +123,17 @@ def _training_error_radicand(future_errors: pd.Series, rho_func: np.vectorize) -
86
123
  if not _finite(future_errors):
87
124
  return float("nan")
88
125
 
126
+ # s_T = 1.48 Med|r_t|, as printed below equation (16) of the paper: 1.48 is the Gaussian
127
+ # consistency factor of the median absolute error, 1 / Phi^-1(3/4) = 1.4826, rounded. The
128
+ # robust Shewhart chart below uses the unrounded value.
89
129
  s_t_median_error = 1.48 * future_errors.abs().median()
90
130
  if not np.isfinite(s_t_median_error) or s_t_median_error <= 0:
91
131
  return float("nan")
92
132
 
93
- return float(np.power(s_t_median_error, 2) * np.nanmean(rho_func(future_errors / s_t_median_error)))
133
+ return float(np.power(s_t_median_error, 2) * np.nanmean(_rho_array(future_errors / s_t_median_error)))
94
134
 
95
135
 
96
- def _tau_from_training_errors(future_errors: pd.Series, rho_func: np.vectorize) -> float:
136
+ def _tau_from_training_errors(future_errors: pd.Series) -> float:
97
137
  """
98
138
  Return the robust scale estimate (tau) for the training-sample errors.
99
139
 
@@ -108,8 +148,6 @@ def _tau_from_training_errors(future_errors: pd.Series, rho_func: np.vectorize)
108
148
  ----------
109
149
  future_errors : pd.Series
110
150
  One-step-ahead errors over the training samples (those after the warm-up).
111
- rho_func : np.vectorize
112
- Element-wise bounded loss applied to the standardised errors.
113
151
 
114
152
  Returns
115
153
  -------
@@ -123,16 +161,15 @@ def _tau_from_training_errors(future_errors: pd.Series, rho_func: np.vectorize)
123
161
  or not positive (the errors carry no usable spread).
124
162
  """
125
163
  if not _finite(future_errors):
126
- # Missing values at or near the start of the series propagate through the
127
- # Holt-Winters recursion and leave every training error NaN.
164
+ # Every training error is NaN when the training period holds no finite observation
165
+ # to forecast against, or when the warm-up statistics themselves are not finite.
128
166
  raise ValueError(
129
167
  f"The control-chart scale estimate is undefined: none of the {future_errors.size} "
130
- "training-sample errors is finite. Missing values at or near the start of the "
131
- "series propagate through the Holt-Winters recursion and leave every error NaN. "
132
- "Supply a series without leading gaps, or pass an explicit positive 's'."
168
+ "training-sample errors is finite, so the series has no finite observation in its "
169
+ "training period to forecast against. Supply a series with more finite values."
133
170
  )
134
171
 
135
- resids = _training_error_radicand(future_errors, rho_func)
172
+ resids = _training_error_radicand(future_errors)
136
173
  if not np.isfinite(resids) or resids <= 0:
137
174
  raise ValueError(
138
175
  f"The control-chart scale estimate is undefined (tau^2 = {resids}), so the control "
@@ -170,6 +207,105 @@ def psi(x: float, k: float = 2.0) -> float:
170
207
  return x if abs(x) < k else k * np.sign(x)
171
208
 
172
209
 
210
+ def _holt_winters_recursion(
211
+ y: np.ndarray, warm_up: Mapping[str, Any], ld_1: float, ld_2: float, ld_s: float
212
+ ) -> dict[str, np.ndarray]:
213
+ """
214
+ Run the robust Holt-Winters recursion over ``y``, one time step per row.
215
+
216
+ Row ``i`` needs row ``i - 1``, so the recursion stays a loop, but over plain numpy arrays
217
+ that are read and filled in place, instead of several ``DataFrame`` lookups per row and a
218
+ ``df.loc`` write of every row. The arithmetic is kept expression for expression, in the
219
+ same order, on the same ``np.float64`` scalars and through the same scalar :func:`rho` and
220
+ :func:`psi`, so the results are bit-for-bit those of the per-row pandas implementation it
221
+ replaces (pinned by ``tests/test_control_chart_hw_golden.py``).
222
+
223
+ Parameters
224
+ ----------
225
+ y : np.ndarray
226
+ The observations, as stored in ``ControlChart.df["y"]``.
227
+ warm_up : Mapping[str, Any]
228
+ ``ControlChart.warm_up``. Row 0 is set from ``y_zero_robust`` (median of the warm-up
229
+ window) and the initial level ``alpha_0``, trend ``beta_0`` and strictly positive
230
+ scale ``sigma_0`` (p 290 of the paper).
231
+ ld_1 : float
232
+ Smoothing parameter for the level, lambda_1.
233
+ ld_2 : float
234
+ Smoothing parameter for the trend, lambda_2.
235
+ ld_s : float
236
+ Smoothing parameter for the scale, lambda_s.
237
+
238
+ Returns
239
+ -------
240
+ dict[str, np.ndarray]
241
+ Float64 arrays of length ``len(y)``, keyed by their ``ControlChart.df`` column:
242
+ ``psi_input``, ``rho_input``, ``y_star``, ``alpha_hat``, ``beta_hat``, ``sigma_hat``
243
+ and ``error``. ``error[0]`` is NaN: row 0 has no one-step-ahead prediction.
244
+ """
245
+ y_zero, alpha_0, beta_0, sigma_0 = (warm_up[key] for key in ("y_zero_robust", "alpha_0", "beta_0", "sigma_0"))
246
+ n = len(y)
247
+ psi_input, rho_input, y_star, alpha_hat, beta_hat, sigma_hat, error = (np.full(n, np.nan) for _ in range(7))
248
+
249
+ rho_input[0] = (y_zero - alpha_0 - beta_0) / sigma_0
250
+ psi_input[0] = rho_input[0]
251
+ y_star[0] = y_zero
252
+ alpha_hat[0] = alpha_0
253
+ beta_hat[0] = beta_0
254
+ sigma_hat[0] = sigma_0
255
+
256
+ for i in range(1, n):
257
+ # Cover the warm-up period, and the rest of the data set. We need that for the residual
258
+ # calculation later anyway. The previous state is read back from the float64 arrays,
259
+ # which yields the same np.float64 scalars the per-row DataFrame lookups returned.
260
+ alpha_prev, beta_prev, sigma_prev = alpha_hat[i - 1], beta_hat[i - 1], sigma_hat[i - 1]
261
+
262
+ # Error = observed - predicted. Predicted = one-step-ahead prediction
263
+ error_i = y[i] - (alpha_prev + beta_prev)
264
+ if np.isnan(error_i):
265
+ # If there is an error, replace it with the median of the last 10 error estimates
266
+ # or as many points as available, filtered to the finite ones so an all-NaN slice
267
+ # never reaches a median (which returns NaN but emits "Mean of empty slice"). (#557)
268
+ #
269
+ # Known limitation, kept for now: the imputed error is a median of ABSOLUTE errors,
270
+ # so it is never negative. Each missing row is treated as an observation above its
271
+ # prediction, which nudges the level up, and over a long gap the trend term
272
+ # compounds that into a drift (with lambdas 0.4 / 0.7, a 40-row gap in N(50, 2) noise
273
+ # took the level from about 49 to 550). Carrying the forecast forward for every
274
+ # missing row, as below, would remove the bias; later gaps keep the imputation so
275
+ # that their results do not change.
276
+ recent_errors = np.abs(error[max(i - 10, 0) : i])
277
+ recent_errors = recent_errors[np.isfinite(recent_errors)]
278
+ if not recent_errors.size:
279
+ # No finite error to impute from: a gap that starts at index 1, the first row
280
+ # with a one-step-ahead error. Imputing NaN used to spread through every later
281
+ # row and make the whole fit fail. Carry the forecast forward instead (the level
282
+ # follows the trend; trend and scale are held) and leave this row's error and
283
+ # cleaned value NaN, so nothing invented reaches the target or the scale.
284
+ alpha_hat[i], beta_hat[i], sigma_hat[i] = alpha_prev + beta_prev, beta_prev, sigma_prev
285
+ continue
286
+ error_i = float(np.median(recent_errors))
287
+ rho_i = error_i / sigma_prev
288
+ prior_variance = np.power(sigma_prev, 2)
289
+ sigma_i = np.sqrt(rho(rho_i) * ld_s * prior_variance + (1.0 - ld_s) * prior_variance)
290
+ psi_i = error_i / sigma_i
291
+ y_star_i = psi(psi_i) * sigma_i + alpha_prev + beta_prev
292
+ alpha_i = ld_1 * y_star_i + (1 - ld_1) * (alpha_prev + beta_prev)
293
+ beta_i = ld_2 * (alpha_i - alpha_prev) + (1 - ld_2) * beta_prev
294
+
295
+ psi_input[i], rho_input[i], y_star[i] = psi_i, rho_i, y_star_i
296
+ alpha_hat[i], beta_hat[i], sigma_hat[i], error[i] = alpha_i, beta_i, sigma_i, error_i
297
+
298
+ return {
299
+ "psi_input": psi_input,
300
+ "rho_input": rho_input,
301
+ "y_star": y_star,
302
+ "alpha_hat": alpha_hat,
303
+ "beta_hat": beta_hat,
304
+ "sigma_hat": sigma_hat,
305
+ "error": error,
306
+ }
307
+
308
+
173
309
  class ControlChart:
174
310
  """Create control chart instance objects."""
175
311
 
@@ -218,6 +354,21 @@ class ControlChart:
218
354
  "CUSUM-style history with Shewhart behaviour.)"
219
355
  )
220
356
 
357
+ self._reset_fit_state()
358
+
359
+ def _reset_fit_state(self) -> None:
360
+ """
361
+ Forget everything an earlier fit set; only the constructor arguments survive.
362
+
363
+ ``calculate_limits`` starts here, so a chart reused for a new series gives exactly
364
+ what a fresh chart would. It used to keep the previous series' ``target`` and ``s``
365
+ and treat them as given, keep its fitted lambdas instead of searching again, and
366
+ fail outright on a series of a different length.
367
+ """
368
+ constructor_arguments = {"style": self.style, "variant": self.variant}
369
+ self.__dict__.clear()
370
+ self.__dict__.update(constructor_arguments)
371
+
221
372
  # Will be calculated by the self.calculate_limits() function
222
373
  self.target: float | None = None
223
374
  self._given_target: float | None = None
@@ -261,7 +412,19 @@ class ControlChart:
261
412
  `target` / `s` are ignored for that small-sample case.
262
413
  Otherwise, if `target` and `s` are numeric, those values are used;
263
414
  if not, they are estimated.
415
+
416
+ Missing values (NaN) in `y` are bridged by imputing a one-step-ahead error equal
417
+ to the median absolute error of the previous 10 rows. That error is never negative,
418
+ so a gap pulls the Holt-Winters level upward, increasingly so for long gaps; fill or
419
+ drop long runs of missing values before fitting. A gap that starts at index 1, the
420
+ first row with a one-step-ahead error, has no earlier error to impute from: those
421
+ rows carry the forecast forward instead (the level follows the trend, the trend and
422
+ scale are held), and their error and cleaned value stay NaN.
423
+
424
+ Every call starts afresh: nothing fitted by an earlier call on the same chart
425
+ (target, ``s``, lambdas, warm-up statistics) is carried into this one.
264
426
  """
427
+ self._reset_fit_state()
265
428
  self._given_target = target
266
429
  self._given_s = s
267
430
  logger.debug(
@@ -285,7 +448,9 @@ class ControlChart:
285
448
  self.df["y"] = y.ravel() if isinstance(y, np.ndarray) else pd.Series(y).values.ravel()
286
449
  self.N = self.df.shape[0]
287
450
 
288
- # Between M = 10 and 20 samples required to warm-up (calculate summary statistics)
451
+ # Between M = 10 and 20 samples required to warm-up (calculate summary statistics).
452
+ # The paper fixes its startup period at m = 10 (p 291); here it grows with N, to 10%
453
+ # of the series, capped at 20. Kept as is: existing results depend on it.
289
454
  self.warm_up["M"] = self.warm_up_M = int(min(20, max(10, np.ceil(0.10 * self.N))))
290
455
 
291
456
  if (self.warm_up_M > self.N) and self.variant.strip().lower() == "hw":
@@ -295,7 +460,7 @@ class ControlChart:
295
460
  self.s = self._tau = self.df["y"].std()
296
461
  self.df["y_star"] = self.df["y"].values
297
462
  self.df["alpha_hat"] = self.target
298
- self.df["beta_hat"] = 0
463
+ self.df["beta_hat"] = 0.0
299
464
  self.df["sigma_hat"] = np.nan
300
465
  self.df["error"] = np.nan
301
466
  return
@@ -377,7 +542,6 @@ class ControlChart:
377
542
  lambda_1 and lambda_2 values. This is done in a 5x5 grid in the code below.
378
543
  """
379
544
  self.ld_s = ld_s = 0.2
380
- rho_func = np.vectorize(rho)
381
545
 
382
546
  # ``is not None``: an explicit ld_1=0.0 (or ld_2=0.0) is a legitimate
383
547
  # user choice, but 0.0 is falsy and a plain truthiness test silently
@@ -398,9 +562,11 @@ class ControlChart:
398
562
  ld_2_index = np.linspace(0.1, 0.9, num=5, endpoint=True)
399
563
  residuals, _ = np.meshgrid(ld_1_index, ld_2_index)
400
564
 
565
+ # The warm-up statistics do not depend on the lambdas: estimate them once, not per cell.
566
+ self._holt_winters_warm_up_statistics()
401
567
  for i, ld_1 in enumerate(ld_1_index):
402
568
  for j, ld_2 in enumerate(ld_2_index):
403
- self._holt_winters_warmup_fit(ld_1=ld_1, ld_2=ld_2, ld_s=ld_s)
569
+ self._holt_winters_smooth(ld_1=ld_1, ld_2=ld_2, ld_s=ld_s)
404
570
 
405
571
  # Apply equation 16 from the paper to the residuals in the 'training' period,
406
572
  # that is the samples after the warm-up period. NaN-aware
@@ -414,7 +580,7 @@ class ControlChart:
414
580
  # An unusable cell records NaN and cannot win the search; the
415
581
  # `np.all(np.isnan(residuals))` check below still catches the case
416
582
  # where every cell is unusable. (#557)
417
- residuals[i, j] = _training_error_radicand(future_errors, rho_func)
583
+ residuals[i, j] = _training_error_radicand(future_errors)
418
584
 
419
585
  if np.all(np.isnan(residuals)):
420
586
  raise ValueError(
@@ -428,11 +594,11 @@ class ControlChart:
428
594
  self.ld_1 = best_ld_1
429
595
  self.ld_2 = best_ld_2
430
596
  self._residuals_HW = residuals
431
- self._holt_winters_warmup_fit(ld_1=best_ld_1, ld_2=best_ld_2, ld_s=ld_s)
597
+ self._holt_winters_smooth(ld_1=best_ld_1, ld_2=best_ld_2, ld_s=ld_s)
432
598
 
433
599
  # Common code for both branches of if-else above
434
600
  future_errors = self.df["error"].iloc[np.asarray(self.train_samples, dtype=int)]
435
- self._tau = _tau_from_training_errors(future_errors, rho_func)
601
+ self._tau = _tau_from_training_errors(future_errors)
436
602
  if self.target is None:
437
603
  # Estimate the target as the median of the y-star (cleaned) y-values
438
604
  self.target = self.df["y_star"].median()
@@ -451,20 +617,34 @@ class ControlChart:
451
617
  See paper: https://onlinelibrary.wiley.com/doi/abs/10.1002/for.1125.
452
618
 
453
619
  Calculates the Holt-Winters fitting and control chart parameters, for given values of the
454
- smoothing parameters lambda_1 (how must local history for the level is used, with values
620
+ smoothing parameters lambda_1 (how much local history for the level is used, with values
455
621
  approaching 1.0 implying that less history is used), and lambda_2 (history for the trend
456
622
  that is used, with lambda_2 approaching 1.0 implying that historical data is less
457
623
  interesting), and lambda_s, a similar parameter for the moving variance of the sequence.
458
624
 
459
625
  lambda_1 = ld_1 = 0.5 (default): value must be between 0 <= ld_1 <= 1.0
460
626
  lambda_2 = ld_2 = 0.8 (default): value must be between 0 <= ld_2 <= 1.0
461
- lambda_s = ld_s = 0.2 (default): value must be between 0 <= ld_2 <= 1.0, based on values
627
+ lambda_s = ld_s = 0.2 (default): value must be between 0 <= ld_s <= 1.0, based on values
462
628
  used in the paper, recommended on page 291.
463
629
 
464
630
  The ideal lambda values (ld_1, ld_2, ld_s) can be found from a grid search.
465
631
  """
466
- df = self.df
467
- y_warm_up = df["y"].iloc[0 : self.warm_up_M]
632
+ self._holt_winters_warm_up_statistics()
633
+ self._holt_winters_smooth(ld_1=ld_1, ld_2=ld_2, ld_s=ld_s)
634
+
635
+ def _holt_winters_warm_up_statistics(self) -> None:
636
+ """
637
+ Estimate the warm-up level, trend and scale (p 290 of the paper) into ``self.warm_up``.
638
+
639
+ They depend only on the warm-up window and on a given ``target`` / ``s``, not on the
640
+ smoothing lambdas, so the lambda grid search estimates them once instead of per cell.
641
+
642
+ Raises
643
+ ------
644
+ ValueError
645
+ If the warm-up window has zero (or non-finite) variance.
646
+ """
647
+ y_warm_up = self.df["y"].iloc[0 : self.warm_up_M]
468
648
  self.warm_up["y_zero_robust"] = y_warm_up.median()
469
649
 
470
650
  if isinstance(self.target, float):
@@ -513,53 +693,10 @@ class ControlChart:
513
693
  "Supply more representative warm-up data, or pass a positive 's'."
514
694
  )
515
695
 
516
- df.loc[0, "rho_input"] = (
517
- self.warm_up["y_zero_robust"] - self.warm_up["alpha_0"] - self.warm_up["beta_0"]
518
- ) / self.warm_up["sigma_0"]
519
- df.loc[0, "psi_input"] = df["rho_input"][0]
520
- df.loc[0, "y_star"] = self.warm_up["y_zero_robust"]
521
- df.loc[0, "alpha_hat"] = self.warm_up["alpha_0"]
522
- df.loc[0, "beta_hat"] = self.warm_up["beta_0"]
523
- df.loc[0, "sigma_hat"] = self.warm_up["sigma_0"]
524
-
525
- for i in range(1, self.N):
526
- # Cover the warm-up period, and the rest of the data set. We need that for the residual
527
- # calculation later anyway.
528
-
529
- # Error = observed - predicted. Predicted = one-step-ahead prediction
530
- error_i = df["y"][i] - (df["alpha_hat"][i - 1] + df["beta_hat"][i - 1])
531
- if np.isnan(error_i):
532
- # If there is an error, replace it with the median of the last 10 error estimates
533
- # or as many points as available. When the gap is at the very start of the series
534
- # there is no finite history to take that median over, so `error_i` stays NaN and
535
- # propagates through alpha_hat / beta_hat / sigma_hat for the rest of the
536
- # recursion; `calculate_limits` raises on that downstream. Filter to the finite
537
- # history explicitly rather than letting an all-NaN slice reach pandas' median,
538
- # which returns NaN but emits "Mean of empty slice". (#557)
539
- recent_errors = df["error"].iloc[max(i - 10, 0) : i].abs().to_numpy(dtype=float)
540
- recent_errors = recent_errors[np.isfinite(recent_errors)]
541
- error_i = float(np.median(recent_errors)) if recent_errors.size else np.nan
542
- rho_i = error_i / df["sigma_hat"][i - 1]
543
- prior_variance = np.power(df["sigma_hat"][i - 1], 2)
544
- sigma_i = np.sqrt(rho(rho_i) * ld_s * prior_variance + (1.0 - ld_s) * prior_variance)
545
- psi_i = error_i / sigma_i
546
- y_star_i = psi(psi_i) * sigma_i + df["alpha_hat"][i - 1] + df["beta_hat"][i - 1]
547
- alpha_i = ld_1 * y_star_i + (1 - ld_1) * (df["alpha_hat"][i - 1] + df["beta_hat"][i - 1])
548
- beta_i = ld_2 * (alpha_i - df["alpha_hat"][i - 1]) + (1 - ld_2) * df["beta_hat"][i - 1]
549
- df.loc[i] = [
550
- df["y"][i],
551
- psi_i,
552
- rho_i,
553
- y_star_i,
554
- alpha_i,
555
- beta_i,
556
- sigma_i,
557
- error_i,
558
- ]
559
-
560
- # Checks: for algorithm debugging:
561
- # future_errors = df["error"]
562
- # S_T_median_error = 1.48 * future_errors.abs().median() # must handle NaNs!
563
- # resids = np.power(S_T_median_error, 2) * \
564
- # np.nanmean((future_errors / S_T_median_error).apply(rho))
565
- # print(np.sqrt(max(0.0, resids)))
696
+ def _holt_winters_smooth(self, ld_1: float, ld_2: float, ld_s: float) -> None:
697
+ """Run the Holt-Winters recursion from ``self.warm_up`` and write its columns into ``self.df``."""
698
+ df = self.df
699
+ columns = _holt_winters_recursion(df["y"].to_numpy(), self.warm_up, ld_1=ld_1, ld_2=ld_2, ld_s=ld_s)
700
+ # One write per column per fit; the "y" column, the column order and the index are untouched.
701
+ for name, values in columns.items():
702
+ df[name] = values