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