processbehavior 0.3.0__tar.gz → 0.3.1__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.0 → processbehavior-0.3.1}/CHANGELOG.md +36 -1
- {processbehavior-0.3.0 → processbehavior-0.3.1}/PKG-INFO +1 -1
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/__init__.py +1 -1
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/derivations.py +200 -81
- processbehavior-0.3.1/tests/test_derivations_robustness.py +315 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/.gitignore +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/CITATION.cff +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/CODE_OF_CONDUCT.md +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/CONTRIBUTING.md +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/LICENSE +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/README.md +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/SECURITY.md +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/examples/README.md +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/analysis.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/analysis_dataset.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/analysis_result.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/calibration.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/capability.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/data_preparation.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/datasets/__init__.py +0 -0
- {processbehavior-0.3.0/tests/fixtures → processbehavior-0.3.1/processbehavior/datasets}/data/coffee_shop_demo_long.csv +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/datasets/loaders.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/datasets/synthetic.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/effects_calculator.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/exceptions.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/exporters/__init__.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/exporters/excel_exporter.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/focused_result.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/formulation_spec.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/loss_function.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/maximum_information.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/__init__.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/_math.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/capability_chart.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/contracts.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/control_chart.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/effects_charts.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/lane_boundaries.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/limits.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/maximum_information_chart.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/plotter.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/renderers.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/report.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/residuals.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/run_rules_viz.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/stats_box.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/themes.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/x_axis_layout.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/plotting/zones.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/process_behavior.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/py.typed +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/residual_calculator.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/result_signals.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/result_tabular.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/sds_detector.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/series_length.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/signals/__init__.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/signals/config.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/signals/detector.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/signals/detectors.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/signals/result.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/spc_constants.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/study.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/processbehavior/types.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/pyproject.toml +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/__init__.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/conftest.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/bishop_analyses/vassds1analysis.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/bishop_analyses/vassds2analysis.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/bishop_analyses/vassds3analysis.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/coffee_capability_full_baseline.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/data/FILLWEIGHTDATA_800.csv +0 -0
- {processbehavior-0.3.0/processbehavior/datasets → processbehavior-0.3.1/tests/fixtures}/data/coffee_shop_demo_long.csv +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/missing_values/X_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/missing_values/X_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/missing_values/X_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/missing_values/mR_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/missing_values/mR_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/missing_values/mR_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xbar_s/S_data.parquet +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/stratified_uneven → processbehavior-0.3.1/tests/fixtures/golden_masters/paired_xbar_s}/S_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xbar_s/S_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_data.parquet +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/stratified_uneven → processbehavior-0.3.1/tests/fixtures/golden_masters/paired_xbar_s}/Xbar_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xbar_s/Xbar_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xmr_r/X_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xmr_r/X_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xmr_r/X_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xmr_r/mR_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xmr_r/mR_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/paired_xmr_r/mR_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/pathological_ordering/X_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/pathological_ordering/X_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/pathological_ordering/X_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/pathological_ordering/mR_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/pathological_ordering/mR_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/pathological_ordering/mR_statistics.json +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/stratified_balanced → processbehavior-0.3.1/tests/fixtures/golden_masters/residual_chart}/S_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/residual_chart/S_metadata.json +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/stratified_balanced → processbehavior-0.3.1/tests/fixtures/golden_masters/residual_chart}/S_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/single_obs_strata/X_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/single_obs_strata/X_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/single_obs_strata/X_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/single_obs_strata/mR_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/single_obs_strata/mR_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/single_obs_strata/mR_statistics.json +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/residual_chart → processbehavior-0.3.1/tests/fixtures/golden_masters/stratified_balanced}/S_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_balanced/S_metadata.json +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/residual_chart → processbehavior-0.3.1/tests/fixtures/golden_masters/stratified_balanced}/S_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_balanced/Xbar_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_balanced/Xbar_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_balanced/Xbar_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_uneven/S_data.parquet +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/paired_xbar_s → processbehavior-0.3.1/tests/fixtures/golden_masters/stratified_uneven}/S_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_uneven/S_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_uneven/Xbar_data.parquet +0 -0
- {processbehavior-0.3.0/tests/fixtures/golden_masters/paired_xbar_s → processbehavior-0.3.1/tests/fixtures/golden_masters/stratified_uneven}/Xbar_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/stratified_uneven/Xbar_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/unstratified_small/X_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/unstratified_small/X_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/unstratified_small/X_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/unstratified_small/mR_data.parquet +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/unstratified_small/mR_metadata.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/fixtures/golden_masters/unstratified_small/mR_statistics.json +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_analysis_bugs.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_analysis_dataset.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_analysis_golden_master.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_analysis_result_contract.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_analysis_specifications.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_api_reference.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_beyond_limits_vectorized.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_bin_extremes.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_bishop_reference.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_by_parameter.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_by_permutations.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_calibration.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_capability.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_capability_chart.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_capability_window.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_chart_availability.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_chart_column_contract.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_chart_parsing.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_control_chart_figure.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_data_preparation.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_datasets.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_derivations.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_design_state_lineage.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_documented_rule_coverage.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_edge_cases.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_effects_calculator.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_effects_plotting.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_encode_rsg_vectorized.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_excel_export.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_execute_validation.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_focus.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_garbage_character_handling.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_init_cleaning.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_lane_boundaries.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_limits_vectorized.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_loss_function.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_maximum_information.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_na_in_factor_columns.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_notebook_ergonomics.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_p0_p2_regressions.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_performance.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_phased_limits.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_plotting.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_plotting_decorations.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_plotting_hardening.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_plotting_xaxis.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_process_behavior.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_recentered_mr_fix.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_reference_integrity.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_release_gate_0_1_0_checklist.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_residual_aliases.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_residual_calculator.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_residual_labels.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_residual_scope.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_resolve_by_grouping.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_result_signals.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_result_tabular.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_sampling_plan.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_sds.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_sds_detector.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_series_length.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_signal_detectors.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_signal_result_surface.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_signals.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_single_detection.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_solo_mr_residuals.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_spc_constants.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_srp_refactor.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_strata_companion.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_strata_sufficiency.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_stratified_insufficiency.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_supports_calibration.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_synthetic.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_types.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_validate_by_parameter.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_weco_integration.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_why_not.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_x_axis_layout.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_xbar_residual_center.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_xbar_s_options.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/tests/test_xbar_stratified.py +0 -0
- {processbehavior-0.3.0 → processbehavior-0.3.1}/validation/PBTESTDATABASE_T100.csv +0 -0
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-09-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Derived variables: ``evaluate`` and ``validate`` now keep their never-raises promise.**
|
|
14
|
+
An adversarial pass (36,000 fuzzed evaluations) found four ways to make them raise from
|
|
15
|
+
pandas: ±inf in the source column (``equal_freq`` / ``equal_width``: "bins must increase
|
|
16
|
+
monotonically"), range labels that collide at six significant digits on a tiny range or a
|
|
17
|
+
near-constant ``sd`` fit ("labels must be unique"), duplicate or null explicit
|
|
18
|
+
``bin_labels``, and a string ``shift`` / ``exponent`` reaching numpy. Now: ±inf leaves the
|
|
19
|
+
bin fit, becomes NaN, and is counted in ``n_invalid`` with a message; range labels gain
|
|
20
|
+
precision until distinct (numbered bins as the last resort, with a message); explicit
|
|
21
|
+
labels must be non-empty, null-free and unique; ``shift``, ``exponent`` and ``breaks`` must
|
|
22
|
+
be finite numbers; ``on_invalid`` must be ``'error'`` or ``'na'``; ``n`` must be a positive
|
|
23
|
+
integer (numpy integers accepted, booleans rejected) — all at construction, as
|
|
24
|
+
``ValidationError``. A datetime or categorical source now evaluates to an all-NA result
|
|
25
|
+
with a message instead of transforming nanoseconds or category codes. A permanent seeded
|
|
26
|
+
fuzz test guards the contract.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- **Every non-finite transform result is a domain violation.** ``log``/``sqrt`` of ``inf``,
|
|
30
|
+
``square`` overflow and ``inverse`` of a denormal used to return ``inf`` with
|
|
31
|
+
``n_invalid=0`` (only ``power`` overflow was flagged); all are now violations, so
|
|
32
|
+
``on_invalid`` applies and an ``inf`` can no longer reach the limits. A z-score of a
|
|
33
|
+
constant (or single-value) column is now a violation for every value rather than a silent
|
|
34
|
+
all-NaN column, so ``on_invalid='error'`` names the derivation at ``formulate``.
|
|
35
|
+
``arcsin`` on values in (1, 100] adds the hint "if these are percentages, divide by 100".
|
|
36
|
+
Requesting more equal-frequency / equal-width bins than there are distinct values now fits
|
|
37
|
+
one bin per distinct value and says so. ``'ordinal'`` labels above five bins say they fell
|
|
38
|
+
back to numbered bins. ``to_dict`` converts numpy scalars and arrays (it was documented
|
|
39
|
+
JSON-safe but was not); ``from_dict`` decodes the ``Infinity``/``NaN`` tags only under
|
|
40
|
+
numeric keys, so a bin labelled ``"NaN"`` stays a string; a missing ``id`` is a
|
|
41
|
+
``ValidationError``, not a ``KeyError``. Finite inputs with distinct labels produce
|
|
42
|
+
byte-identical edges, labels and values.
|
|
43
|
+
|
|
10
44
|
## [0.3.0] - 2026-09-14
|
|
11
45
|
|
|
12
46
|
### Added
|
|
@@ -869,7 +903,8 @@ and may change between minor versions until 1.0.
|
|
|
869
903
|
- `natsort >= 8.0`
|
|
870
904
|
- `plotly >= 5.18, < 7`
|
|
871
905
|
|
|
872
|
-
[Unreleased]: https://github.com/cnicholas/processbehavior/compare/v0.3.
|
|
906
|
+
[Unreleased]: https://github.com/cnicholas/processbehavior/compare/v0.3.1...HEAD
|
|
907
|
+
[0.3.1]: https://github.com/cnicholas/processbehavior/compare/v0.3.0...v0.3.1
|
|
873
908
|
[0.3.0]: https://github.com/cnicholas/processbehavior/compare/v0.2.1...v0.3.0
|
|
874
909
|
[0.2.1]: https://github.com/cnicholas/processbehavior/compare/v0.2.0...v0.2.1
|
|
875
910
|
[0.2.0]: https://github.com/cnicholas/processbehavior/compare/v0.1.1...v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: processbehavior
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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.1'
|
|
51
51
|
|
|
52
52
|
# Result object
|
|
53
53
|
from processbehavior.analysis_result import AnalysisResult
|
|
@@ -27,6 +27,7 @@ registry leaves a clean slot to add it behind an optional import later.
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
29
|
import math
|
|
30
|
+
import numbers
|
|
30
31
|
import uuid
|
|
31
32
|
from collections.abc import Callable
|
|
32
33
|
from dataclasses import dataclass, field, replace
|
|
@@ -66,9 +67,14 @@ _ORDINAL_LABELS: dict[int, list[str]] = {
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def _jsonify(obj):
|
|
69
|
-
"""Recursively make a value JSON-safe
|
|
70
|
+
"""Recursively make a value JSON-safe: numpy scalars/arrays become Python values,
|
|
71
|
+
non-finite floats become the tags ``'Infinity'`` / ``'-Infinity'`` / ``'NaN'``."""
|
|
70
72
|
if isinstance(obj, bool):
|
|
71
73
|
return obj
|
|
74
|
+
if isinstance(obj, np.ndarray):
|
|
75
|
+
return _jsonify(obj.tolist())
|
|
76
|
+
if isinstance(obj, np.generic):
|
|
77
|
+
return _jsonify(obj.item())
|
|
72
78
|
if isinstance(obj, float):
|
|
73
79
|
if math.isinf(obj):
|
|
74
80
|
return 'Infinity' if obj > 0 else '-Infinity'
|
|
@@ -82,18 +88,20 @@ def _jsonify(obj):
|
|
|
82
88
|
return obj
|
|
83
89
|
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
# Keys whose values are numbers (so the non-finite tags decode there, and only there).
|
|
92
|
+
# Label lists are never decoded: a bin labelled "NaN" or "Infinity" stays a string.
|
|
93
|
+
_NUMERIC_KEYS = frozenset({'edges', 'breaks', 'mu', 'sigma', 'shift', 'exponent', 'n_bins'})
|
|
94
|
+
_FLOAT_TAGS = {'Infinity': math.inf, '-Infinity': -math.inf, 'NaN': math.nan}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _dejsonify(obj, numeric: bool = False):
|
|
98
|
+
"""Inverse of :func:`_jsonify`. Tags decode only under numeric keys."""
|
|
99
|
+
if isinstance(obj, str):
|
|
100
|
+
return _FLOAT_TAGS[obj] if numeric and obj in _FLOAT_TAGS else obj
|
|
93
101
|
if isinstance(obj, dict):
|
|
94
|
-
return {k: _dejsonify(v) for k, v in obj.items()}
|
|
102
|
+
return {k: _dejsonify(v, numeric=(k in _NUMERIC_KEYS)) for k, v in obj.items()}
|
|
95
103
|
if isinstance(obj, list):
|
|
96
|
-
return [_dejsonify(v) for v in obj]
|
|
104
|
+
return [_dejsonify(v, numeric=numeric) for v in obj]
|
|
97
105
|
return obj
|
|
98
106
|
|
|
99
107
|
|
|
@@ -143,6 +151,32 @@ class ValidationResult:
|
|
|
143
151
|
return '; '.join(i.get('message', i.get('code', '')) for i in self.issues)
|
|
144
152
|
|
|
145
153
|
|
|
154
|
+
def _validate_bin_labels(bl) -> None:
|
|
155
|
+
"""A style name, or an explicit list of names that is non-empty, null-free and unique."""
|
|
156
|
+
if isinstance(bl, str):
|
|
157
|
+
if bl not in BIN_LABEL_STYLES:
|
|
158
|
+
raise ValidationError(
|
|
159
|
+
f'Unknown bin_labels style {bl!r}. Use one of {list(BIN_LABEL_STYLES)} '
|
|
160
|
+
'or an explicit list of names.'
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
if not isinstance(bl, (list, tuple)):
|
|
164
|
+
raise ValidationError(f'bin_labels must be a style name or a list of names, got {type(bl).__name__}.')
|
|
165
|
+
names = list(bl)
|
|
166
|
+
if not names:
|
|
167
|
+
raise ValidationError('Explicit bin_labels must not be empty.')
|
|
168
|
+
if any(v is None or (isinstance(v, float) and math.isnan(v)) for v in names):
|
|
169
|
+
raise ValidationError('Explicit bin_labels must not contain null values.')
|
|
170
|
+
if len({str(v) for v in names}) != len(names):
|
|
171
|
+
raise ValidationError(f'Explicit bin_labels must be unique, got {names!r}.')
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _require_finite_real(value, name: str) -> None:
|
|
175
|
+
"""Raise ValidationError unless ``value`` is a finite real number (bools excluded)."""
|
|
176
|
+
if isinstance(value, bool) or not isinstance(value, numbers.Real) or not math.isfinite(float(value)):
|
|
177
|
+
raise ValidationError(f'{name} must be a finite number, got {value!r}.')
|
|
178
|
+
|
|
179
|
+
|
|
146
180
|
# ============================================================================
|
|
147
181
|
# Derivation spec
|
|
148
182
|
# ============================================================================
|
|
@@ -170,49 +204,50 @@ class Derivation:
|
|
|
170
204
|
def __post_init__(self) -> None:
|
|
171
205
|
if not self.column or not isinstance(self.column, str):
|
|
172
206
|
raise ValidationError('Derivation.column must be a non-empty string.')
|
|
173
|
-
|
|
174
207
|
if self.family == 'transform':
|
|
175
|
-
|
|
176
|
-
raise ValidationError(
|
|
177
|
-
f"Unknown transform function {self.function!r}. "
|
|
178
|
-
f'Supported: {list(TRANSFORM_FUNCTIONS)} (ln is an alias for log).'
|
|
179
|
-
)
|
|
180
|
-
if self.function == 'power' and 'exponent' not in self.params:
|
|
181
|
-
raise ValidationError("transform 'power' requires an 'exponent' param.")
|
|
182
|
-
|
|
208
|
+
self._validate_transform()
|
|
183
209
|
elif self.family == 'bin':
|
|
184
|
-
|
|
185
|
-
raise ValidationError(
|
|
186
|
-
f"bin derivations must have function='bin', got {self.function!r}."
|
|
187
|
-
)
|
|
188
|
-
method = self.params.get('method')
|
|
189
|
-
if method not in BIN_METHODS:
|
|
190
|
-
raise ValidationError(
|
|
191
|
-
f"Unknown bin method {method!r}. Supported: {list(BIN_METHODS)}."
|
|
192
|
-
)
|
|
193
|
-
if method == 'breaks':
|
|
194
|
-
breaks = self.params.get('breaks')
|
|
195
|
-
if not breaks or len(breaks) < 1 or not _ascending(breaks):
|
|
196
|
-
raise ValidationError(
|
|
197
|
-
"bin method 'breaks' requires an ascending list of cut points."
|
|
198
|
-
)
|
|
199
|
-
elif method in ('equal_freq', 'equal_width'):
|
|
200
|
-
n = self.params.get('n')
|
|
201
|
-
if not (isinstance(n, int) and n > 0):
|
|
202
|
-
raise ValidationError(
|
|
203
|
-
f"bin method {method!r} requires an integer n > 0, got {n!r}."
|
|
204
|
-
)
|
|
205
|
-
bl = self.params.get('bin_labels', 'range')
|
|
206
|
-
if isinstance(bl, str) and bl not in BIN_LABEL_STYLES:
|
|
207
|
-
raise ValidationError(
|
|
208
|
-
f"Unknown bin_labels style {bl!r}. Use one of {list(BIN_LABEL_STYLES)} "
|
|
209
|
-
'or an explicit list of names.'
|
|
210
|
-
)
|
|
210
|
+
self._validate_bin()
|
|
211
211
|
else:
|
|
212
212
|
raise ValidationError(
|
|
213
213
|
f"Derivation.family must be 'transform' or 'bin', got {self.family!r}."
|
|
214
214
|
)
|
|
215
215
|
|
|
216
|
+
def _validate_transform(self) -> None:
|
|
217
|
+
if self.function not in TRANSFORM_FUNCTIONS:
|
|
218
|
+
raise ValidationError(
|
|
219
|
+
f"Unknown transform function {self.function!r}. "
|
|
220
|
+
f'Supported: {list(TRANSFORM_FUNCTIONS)} (ln is an alias for log).'
|
|
221
|
+
)
|
|
222
|
+
if self.function == 'power' and 'exponent' not in self.params:
|
|
223
|
+
raise ValidationError("transform 'power' requires an 'exponent' param.")
|
|
224
|
+
on_invalid = self.params.get('on_invalid', 'error')
|
|
225
|
+
if on_invalid not in ('error', 'na'):
|
|
226
|
+
raise ValidationError(f"on_invalid must be 'error' or 'na', got {on_invalid!r}.")
|
|
227
|
+
for key in ('shift', 'exponent'):
|
|
228
|
+
if self.params.get(key) is not None:
|
|
229
|
+
_require_finite_real(self.params[key], key)
|
|
230
|
+
|
|
231
|
+
def _validate_bin(self) -> None:
|
|
232
|
+
if self.function != 'bin':
|
|
233
|
+
raise ValidationError(f"bin derivations must have function='bin', got {self.function!r}.")
|
|
234
|
+
method = self.params.get('method')
|
|
235
|
+
if method not in BIN_METHODS:
|
|
236
|
+
raise ValidationError(f'Unknown bin method {method!r}. Supported: {list(BIN_METHODS)}.')
|
|
237
|
+
if method == 'breaks':
|
|
238
|
+
breaks = self.params.get('breaks')
|
|
239
|
+
if not breaks:
|
|
240
|
+
raise ValidationError("bin method 'breaks' requires an ascending list of cut points.")
|
|
241
|
+
for b in breaks:
|
|
242
|
+
_require_finite_real(b, 'breaks')
|
|
243
|
+
if not _ascending(breaks):
|
|
244
|
+
raise ValidationError("bin method 'breaks' requires an ascending list of cut points.")
|
|
245
|
+
elif method in ('equal_freq', 'equal_width'):
|
|
246
|
+
n = self.params.get('n')
|
|
247
|
+
if isinstance(n, bool) or not isinstance(n, numbers.Integral) or n <= 0:
|
|
248
|
+
raise ValidationError(f'bin method {method!r} requires an integer n > 0, got {n!r}.')
|
|
249
|
+
_validate_bin_labels(self.params.get('bin_labels', 'range'))
|
|
250
|
+
|
|
216
251
|
# -- factories ---------------------------------------------------------
|
|
217
252
|
@classmethod
|
|
218
253
|
def transform(
|
|
@@ -229,9 +264,11 @@ class Derivation:
|
|
|
229
264
|
fn = 'log' if function == 'ln' else function
|
|
230
265
|
params: dict = {'on_invalid': on_invalid}
|
|
231
266
|
if shift is not None:
|
|
232
|
-
|
|
267
|
+
_require_finite_real(shift, 'shift')
|
|
268
|
+
params['shift'] = float(shift)
|
|
233
269
|
if exponent is not None:
|
|
234
|
-
|
|
270
|
+
_require_finite_real(exponent, 'exponent')
|
|
271
|
+
params['exponent'] = float(exponent)
|
|
235
272
|
return cls(family='transform', column=column, function=fn, label=label, params=params)
|
|
236
273
|
|
|
237
274
|
@classmethod
|
|
@@ -247,11 +284,20 @@ class Derivation:
|
|
|
247
284
|
right: bool = False,
|
|
248
285
|
) -> Derivation:
|
|
249
286
|
"""Build a binning spec."""
|
|
250
|
-
|
|
287
|
+
if isinstance(bin_labels, (list, tuple)):
|
|
288
|
+
bin_labels = [v.item() if isinstance(v, np.generic) else v for v in bin_labels]
|
|
289
|
+
params: dict = {'method': method, 'bin_labels': bin_labels, 'right': bool(right)}
|
|
251
290
|
if method == 'breaks':
|
|
252
|
-
|
|
291
|
+
if breaks is not None:
|
|
292
|
+
for b in breaks:
|
|
293
|
+
_require_finite_real(b, 'breaks')
|
|
294
|
+
params['breaks'] = [float(b) for b in breaks]
|
|
295
|
+
else:
|
|
296
|
+
params['breaks'] = None
|
|
253
297
|
else:
|
|
254
|
-
|
|
298
|
+
if isinstance(n, bool) or not isinstance(n, numbers.Integral):
|
|
299
|
+
raise ValidationError(f'bin method {method!r} requires an integer n > 0, got {n!r}.')
|
|
300
|
+
params['n'] = int(n)
|
|
255
301
|
return cls(family='bin', column=column, function='bin', label=label, params=params)
|
|
256
302
|
|
|
257
303
|
# -- naming / serialization -------------------------------------------
|
|
@@ -275,6 +321,8 @@ class Derivation:
|
|
|
275
321
|
@classmethod
|
|
276
322
|
def from_dict(cls, d: dict) -> Derivation:
|
|
277
323
|
"""Reconstruct from :meth:`to_dict`. Takes ``id`` from the dict (never mints)."""
|
|
324
|
+
if 'id' not in d:
|
|
325
|
+
raise ValidationError("Derivation.from_dict needs an 'id' (use to_dict() output).")
|
|
278
326
|
return cls(
|
|
279
327
|
family=d['family'],
|
|
280
328
|
column=d['column'],
|
|
@@ -341,7 +389,11 @@ _TRANSFORM_REGISTRY: dict[str, tuple[Callable, Callable]] = {
|
|
|
341
389
|
|
|
342
390
|
def _evaluate_transform(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
343
391
|
x = pd.to_numeric(col, errors='coerce').astype('float64')
|
|
392
|
+
finite = np.isfinite(x.to_numpy())
|
|
344
393
|
present = x.notna()
|
|
394
|
+
# ±inf is a domain violation for every transform: no finite result exists.
|
|
395
|
+
nonfinite_in = present & ~finite
|
|
396
|
+
present = present & finite
|
|
345
397
|
params = spec.params
|
|
346
398
|
shift = params.get('shift')
|
|
347
399
|
if shift is not None:
|
|
@@ -351,7 +403,7 @@ def _evaluate_transform(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
|
351
403
|
message: str | None = None
|
|
352
404
|
|
|
353
405
|
if spec.function == 'zscore':
|
|
354
|
-
|
|
406
|
+
violation = pd.Series(False, index=x.index)
|
|
355
407
|
vals = x[present]
|
|
356
408
|
mu = float(vals.mean()) if present.any() else math.nan
|
|
357
409
|
sigma = float(vals.std(ddof=1)) if present.sum() > 1 else math.nan
|
|
@@ -359,21 +411,30 @@ def _evaluate_transform(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
|
359
411
|
with np.errstate(all='ignore'):
|
|
360
412
|
y = (x - mu) / sigma
|
|
361
413
|
if not (sigma and math.isfinite(sigma) and sigma > 0):
|
|
414
|
+
# No z-score exists for any value, so every present value is a violation
|
|
415
|
+
# (on_invalid then applies) rather than a silent all-NaN column.
|
|
362
416
|
message = 'zero or undefined variance; zscore is undefined'
|
|
417
|
+
violation = present.copy()
|
|
363
418
|
|
|
364
419
|
elif spec.function == 'power':
|
|
365
420
|
exponent = params['exponent']
|
|
366
421
|
with np.errstate(all='ignore'):
|
|
367
422
|
y = pd.Series(np.power(x.to_numpy(), exponent), index=x.index)
|
|
368
|
-
|
|
369
|
-
# finite real (negative base to a fractional power, 0 to a negative power).
|
|
370
|
-
violation = present & ~np.isfinite(y)
|
|
423
|
+
violation = pd.Series(False, index=x.index)
|
|
371
424
|
|
|
372
425
|
else:
|
|
373
426
|
fn, domain = _TRANSFORM_REGISTRY[spec.function]
|
|
374
427
|
clamped, violation = domain(x, present)
|
|
375
428
|
with np.errstate(all='ignore'):
|
|
376
429
|
y = pd.Series(fn(clamped.to_numpy()), index=x.index)
|
|
430
|
+
if spec.function == 'arcsin' and violation.any():
|
|
431
|
+
xv = x[present]
|
|
432
|
+
if len(xv) and xv.max() > 1.0 and xv.min() >= 0.0 and xv.max() <= 100.0:
|
|
433
|
+
message = 'values exceed 1; if these are percentages, divide by 100 first'
|
|
434
|
+
|
|
435
|
+
# A result that is not a finite real is a domain violation, for every function:
|
|
436
|
+
# negative base to a fractional power, 0 to a negative power, overflow to inf.
|
|
437
|
+
violation = violation | (present & ~np.isfinite(y.to_numpy(dtype=float))) | nonfinite_in
|
|
377
438
|
|
|
378
439
|
# Violations -> NaN in the output (NA inputs already produce NaN).
|
|
379
440
|
y = y.where(~violation)
|
|
@@ -393,15 +454,16 @@ def _fmt(v: float) -> str:
|
|
|
393
454
|
return f'{v:g}'
|
|
394
455
|
|
|
395
456
|
|
|
396
|
-
def _range_labels(edges, right: bool) -> list[str]:
|
|
457
|
+
def _range_labels(edges, right: bool, digits: int = 6) -> list[str]:
|
|
397
458
|
labels = []
|
|
398
459
|
count = len(edges) - 1
|
|
460
|
+
fmt = lambda v: f'{v:.{digits}g}' # noqa: E731
|
|
399
461
|
for i in range(count):
|
|
400
462
|
a, b = edges[i], edges[i + 1]
|
|
401
463
|
if math.isinf(a):
|
|
402
|
-
labels.append(f'< {
|
|
464
|
+
labels.append(f'< {fmt(b)}' if not right else f'<= {fmt(b)}')
|
|
403
465
|
elif math.isinf(b):
|
|
404
|
-
labels.append(f'>= {
|
|
466
|
+
labels.append(f'>= {fmt(a)}' if not right else f'> {fmt(a)}')
|
|
405
467
|
else:
|
|
406
468
|
left = '[' if not right else '('
|
|
407
469
|
rb = ')' if not right else ']'
|
|
@@ -411,30 +473,49 @@ def _range_labels(edges, right: bool) -> list[str]:
|
|
|
411
473
|
rb = ']'
|
|
412
474
|
if right and i == 0:
|
|
413
475
|
left = '['
|
|
414
|
-
labels.append(f'{left}{
|
|
476
|
+
labels.append(f'{left}{fmt(a)}, {fmt(b)}{rb}')
|
|
415
477
|
return labels
|
|
416
478
|
|
|
417
479
|
|
|
480
|
+
def _unique_range_labels(edges, right: bool):
|
|
481
|
+
"""Range labels that are guaranteed distinct. Returns (labels, message).
|
|
482
|
+
|
|
483
|
+
Six significant digits read well but collide on a tiny range (edges 1.00000001 and
|
|
484
|
+
1.00000002 both print as ``1``), and pd.cut refuses duplicate labels. Add precision
|
|
485
|
+
until the labels differ; if even 17 digits cannot separate them, number the bins.
|
|
486
|
+
"""
|
|
487
|
+
for digits in range(6, 18):
|
|
488
|
+
labels = _range_labels(edges, right, digits)
|
|
489
|
+
if len(set(labels)) == len(labels):
|
|
490
|
+
return labels, None
|
|
491
|
+
return [f'Bin {i + 1}' for i in range(len(edges) - 1)], (
|
|
492
|
+
'bin edges too close to label as ranges; using numbered bins'
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
418
496
|
def _bin_label_names(bin_labels, edges, right: bool):
|
|
419
497
|
"""Resolve the ordered label names for the fitted edges. Returns (labels, message)."""
|
|
420
498
|
count = len(edges) - 1
|
|
421
499
|
if isinstance(bin_labels, (list, tuple)):
|
|
422
500
|
if len(bin_labels) != count:
|
|
423
|
-
|
|
501
|
+
labels, _ = _unique_range_labels(edges, right)
|
|
502
|
+
return labels, (
|
|
424
503
|
f'{len(bin_labels)} labels supplied but {count} bins fitted; using range labels'
|
|
425
504
|
)
|
|
426
505
|
return list(bin_labels), None
|
|
427
506
|
if bin_labels == 'ordinal':
|
|
428
507
|
if count in _ORDINAL_LABELS:
|
|
429
508
|
return list(_ORDINAL_LABELS[count]), None
|
|
430
|
-
return [f'Bin {i + 1}' for i in range(count)],
|
|
509
|
+
return [f'Bin {i + 1}' for i in range(count)], (
|
|
510
|
+
f'ordinal labels are defined for 2 to 5 bins; {count} fitted, using numbered bins'
|
|
511
|
+
)
|
|
431
512
|
if bin_labels == 'number':
|
|
432
513
|
return [f'Bin {i + 1}' for i in range(count)], None
|
|
433
|
-
return
|
|
514
|
+
return _unique_range_labels(edges, right)
|
|
434
515
|
|
|
435
516
|
|
|
436
517
|
def _fit_edges(spec: Derivation, present_vals: pd.Series):
|
|
437
|
-
"""Return (edges, message) for the requested method, fitted on non-NA values."""
|
|
518
|
+
"""Return (edges, message) for the requested method, fitted on finite non-NA values."""
|
|
438
519
|
params = spec.params
|
|
439
520
|
method = params['method']
|
|
440
521
|
message = None
|
|
@@ -445,30 +526,42 @@ def _fit_edges(spec: Derivation, present_vals: pd.Series):
|
|
|
445
526
|
mu = float(present_vals.mean())
|
|
446
527
|
sigma = float(present_vals.std(ddof=1)) if len(present_vals) > 1 else math.nan
|
|
447
528
|
edges = [-math.inf, mu - 2 * sigma, mu - sigma, mu + sigma, mu + 2 * sigma, math.inf]
|
|
448
|
-
|
|
449
|
-
n = params['n']
|
|
450
|
-
lo, hi = float(present_vals.min()), float(present_vals.max())
|
|
451
|
-
edges = list(np.linspace(lo, hi, n + 1))
|
|
452
|
-
else: # equal_freq
|
|
529
|
+
else:
|
|
453
530
|
n = params['n']
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
message = f'requested {n} bins,
|
|
531
|
+
n_distinct = int(present_vals.nunique())
|
|
532
|
+
if n_distinct and n > n_distinct:
|
|
533
|
+
# More bins than distinct values can only produce empty bins.
|
|
534
|
+
message = f'requested {n} bins, only {n_distinct} distinct values; fitted {n_distinct}'
|
|
535
|
+
n = n_distinct
|
|
536
|
+
if method == 'equal_width':
|
|
537
|
+
lo, hi = float(present_vals.min()), float(present_vals.max())
|
|
538
|
+
edges = list(np.linspace(lo, hi, n + 1))
|
|
539
|
+
else: # equal_freq
|
|
540
|
+
qs = np.linspace(0.0, 1.0, n + 1)
|
|
541
|
+
edges = list(np.unique(np.quantile(present_vals, qs)))
|
|
542
|
+
if len(edges) - 1 != n:
|
|
543
|
+
message = f'requested {params["n"]} bins, ties produced {len(edges) - 1}'
|
|
458
544
|
return edges, message
|
|
459
545
|
|
|
460
546
|
|
|
461
547
|
def _evaluate_bin(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
462
548
|
x = pd.to_numeric(col, errors='coerce').astype('float64')
|
|
463
|
-
|
|
549
|
+
finite = np.isfinite(x.to_numpy())
|
|
550
|
+
# ±inf cannot be placed in any finite bin: it leaves the fit, becomes NaN in the
|
|
551
|
+
# output, and is counted (the bin analogue of a transform's domain violation).
|
|
552
|
+
nonfinite = x.notna() & ~finite
|
|
553
|
+
present = x.notna() & finite
|
|
554
|
+
n_nonfinite = int(nonfinite.sum())
|
|
555
|
+
nonfinite_msg = f'{n_nonfinite} non-finite value(s) cannot be binned' if n_nonfinite else None
|
|
464
556
|
params = spec.params
|
|
465
557
|
right = params.get('right', False)
|
|
466
|
-
empty_index = x.index[[]]
|
|
467
558
|
|
|
468
559
|
if present.sum() == 0:
|
|
469
560
|
return EvalResult(
|
|
470
561
|
values=pd.Series(pd.Categorical([np.nan] * len(x)), index=x.index),
|
|
471
|
-
n_invalid=
|
|
562
|
+
n_invalid=n_nonfinite, invalid_index=x.index[nonfinite],
|
|
563
|
+
fitted={'method': params['method'], 'n_bins': 0, 'edges': [], 'labels': []},
|
|
564
|
+
message='; '.join(m for m in ('no finite values to bin', nonfinite_msg) if m),
|
|
472
565
|
)
|
|
473
566
|
|
|
474
567
|
edges, fit_msg = _fit_edges(spec, x[present])
|
|
@@ -488,9 +581,9 @@ def _evaluate_bin(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
|
488
581
|
if degenerate:
|
|
489
582
|
return EvalResult(
|
|
490
583
|
values=pd.Series(pd.Categorical([np.nan] * len(x)), index=x.index),
|
|
491
|
-
n_invalid=
|
|
584
|
+
n_invalid=n_nonfinite, invalid_index=x.index[nonfinite],
|
|
492
585
|
fitted={'method': params['method'], 'n_bins': 0, 'edges': [], 'labels': []},
|
|
493
|
-
message='column has no spread; cannot bin',
|
|
586
|
+
message='; '.join(m for m in ('column has no spread; cannot bin', nonfinite_msg) if m),
|
|
494
587
|
)
|
|
495
588
|
|
|
496
589
|
labels, label_msg = _bin_label_names(params.get('bin_labels', 'range'), edges, right)
|
|
@@ -507,7 +600,9 @@ def _evaluate_bin(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
|
507
600
|
if not right and math.isfinite(cut_edges[-1]):
|
|
508
601
|
cut_edges[-1] = float(np.nextafter(cut_edges[-1], math.inf))
|
|
509
602
|
|
|
510
|
-
cats = pd.cut(
|
|
603
|
+
cats = pd.cut(
|
|
604
|
+
x.where(present), bins=cut_edges, right=right, labels=labels, include_lowest=True, ordered=True
|
|
605
|
+
)
|
|
511
606
|
values = pd.Series(cats, index=x.index)
|
|
512
607
|
|
|
513
608
|
fitted = {
|
|
@@ -521,9 +616,10 @@ def _evaluate_bin(spec: Derivation, col: pd.Series) -> EvalResult:
|
|
|
521
616
|
fitted['mu'] = float(x[present].mean())
|
|
522
617
|
fitted['sigma'] = float(x[present].std(ddof=1)) if present.sum() > 1 else math.nan
|
|
523
618
|
|
|
524
|
-
message = '; '.join(m for m in (fit_msg, label_msg) if m) or None
|
|
619
|
+
message = '; '.join(m for m in (fit_msg, label_msg, nonfinite_msg) if m) or None
|
|
525
620
|
return EvalResult(
|
|
526
|
-
values=values, n_invalid=
|
|
621
|
+
values=values, n_invalid=n_nonfinite, invalid_index=x.index[nonfinite], fitted=fitted,
|
|
622
|
+
message=message,
|
|
527
623
|
)
|
|
528
624
|
|
|
529
625
|
|
|
@@ -538,11 +634,34 @@ def evaluate(spec: Derivation, column: pd.Series) -> EvalResult:
|
|
|
538
634
|
Pre-existing NA passes through as NA; ``n_invalid`` counts only domain
|
|
539
635
|
violations among non-NA values. Never raises.
|
|
540
636
|
"""
|
|
637
|
+
if not _is_numeric_source(column):
|
|
638
|
+
empty = column.index[[]]
|
|
639
|
+
if spec.family == 'bin':
|
|
640
|
+
values = pd.Series(pd.Categorical([np.nan] * len(column)), index=column.index)
|
|
641
|
+
fitted = {'method': spec.params['method'], 'n_bins': 0, 'edges': [], 'labels': []}
|
|
642
|
+
else:
|
|
643
|
+
values = pd.Series(np.nan, index=column.index, dtype='float64')
|
|
644
|
+
fitted = {}
|
|
645
|
+
return EvalResult(
|
|
646
|
+
values=values, n_invalid=0, invalid_index=empty, fitted=fitted,
|
|
647
|
+
message=f'column {column.name!r} is not numeric; nothing derived',
|
|
648
|
+
)
|
|
541
649
|
if spec.family == 'transform':
|
|
542
650
|
return _evaluate_transform(spec, column)
|
|
543
651
|
return _evaluate_bin(spec, column)
|
|
544
652
|
|
|
545
653
|
|
|
654
|
+
def _is_numeric_source(column: pd.Series) -> bool:
|
|
655
|
+
"""Numeric (bool included) or object/string that coerces to numbers; not datetime/categorical."""
|
|
656
|
+
dtype = column.dtype
|
|
657
|
+
if pd.api.types.is_bool_dtype(dtype) or pd.api.types.is_numeric_dtype(dtype):
|
|
658
|
+
return True
|
|
659
|
+
if pd.api.types.is_object_dtype(dtype) or pd.api.types.is_string_dtype(dtype):
|
|
660
|
+
coerced = pd.to_numeric(column, errors='coerce')
|
|
661
|
+
return bool(coerced.notna().any()) or not bool(column.notna().any())
|
|
662
|
+
return False
|
|
663
|
+
|
|
664
|
+
|
|
546
665
|
def validate(spec: Derivation, dataset: pd.DataFrame, existing_names=None) -> ValidationResult:
|
|
547
666
|
"""Structured pre-commit check (label collision, dtype, bin-label count, breaks order).
|
|
548
667
|
|