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