spc-lib 1.0.0__py3-none-any.whl
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.
- spc_lib/__init__.py +17 -0
- spc_lib/charts/__init__.py +14 -0
- spc_lib/charts/attributes.py +147 -0
- spc_lib/charts/time_weighted.py +412 -0
- spc_lib/charts/variables.py +262 -0
- spc_lib/core/__init__.py +0 -0
- spc_lib/core/base_chart.py +113 -0
- spc_lib/metrics/__init__.py +0 -0
- spc_lib/metrics/arl.py +0 -0
- spc_lib/rules/__init__.py +3 -0
- spc_lib/rules/western_electric.py +145 -0
- spc_lib/stats/__init__.py +3 -0
- spc_lib/stats/diagnostics.py +130 -0
- spc_lib/visualization/__init__.py +13 -0
- spc_lib/visualization/plotly_engine.py +493 -0
- spc_lib-1.0.0.dist-info/METADATA +34 -0
- spc_lib-1.0.0.dist-info/RECORD +19 -0
- spc_lib-1.0.0.dist-info/WHEEL +4 -0
- spc_lib-1.0.0.dist-info/licenses/LICENSE +21 -0
spc_lib/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .charts.variables import XBarRChart, XBarSChart, IMRChart
|
|
2
|
+
from .charts.attributes import PChart, CChart
|
|
3
|
+
from .charts.time_weighted import CUSUMChart, CUSUMVarianceChart, EWMAChart
|
|
4
|
+
from .visualization.plotly_engine import plot_control_chart, plot_rules_violations
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"XBarRChart",
|
|
8
|
+
"XBarSChart",
|
|
9
|
+
"IMRChart",
|
|
10
|
+
"PChart",
|
|
11
|
+
"CChart",
|
|
12
|
+
"CUSUMChart",
|
|
13
|
+
"CUSUMVarianceChart",
|
|
14
|
+
"EWMAChart",
|
|
15
|
+
"plot_control_chart",
|
|
16
|
+
"plot_rules_violations"
|
|
17
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .variables import XBarRChart, XBarSChart, IMRChart
|
|
2
|
+
from .attributes import PChart, CChart
|
|
3
|
+
from .time_weighted import CUSUMChart, CUSUMVarianceChart, EWMAChart
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"XBarRChart",
|
|
7
|
+
"XBarSChart",
|
|
8
|
+
"IMRChart",
|
|
9
|
+
"PChart",
|
|
10
|
+
"CChart",
|
|
11
|
+
"CUSUMChart",
|
|
12
|
+
"CUSUMVarianceChart",
|
|
13
|
+
"EWMAChart"
|
|
14
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from src.spc_lib.core.base_chart import BaseControlChart
|
|
3
|
+
|
|
4
|
+
class PChart(BaseControlChart):
|
|
5
|
+
"""
|
|
6
|
+
p-карта (контрольная карта доли дефектных изделий).
|
|
7
|
+
"""
|
|
8
|
+
def __init__(self, data, n_fixed=None, datetimes=None, target=None, usl=None, lsl=None):
|
|
9
|
+
super().__init__(data, datetimes, target, usl, lsl)
|
|
10
|
+
self.n_fixed = n_fixed
|
|
11
|
+
self.main_label = "p-карта: Доля дефектных единиц"
|
|
12
|
+
self.disp_label = None
|
|
13
|
+
|
|
14
|
+
self.p_values = None
|
|
15
|
+
self.n_values = None
|
|
16
|
+
self.p_bar = None
|
|
17
|
+
self.n_bar = None
|
|
18
|
+
|
|
19
|
+
def fit(self, baseline_mask=None, method='classic', use_average_n=False):
|
|
20
|
+
"""
|
|
21
|
+
use_average_n: bool
|
|
22
|
+
Если True, для переменного размера выборки будут вычислены
|
|
23
|
+
константные (прямые) границы на основе среднего размера выборки (n_bar).
|
|
24
|
+
"""
|
|
25
|
+
if baseline_mask is None:
|
|
26
|
+
baseline_mask = np.ones(self.n_subgroups, dtype=bool)
|
|
27
|
+
|
|
28
|
+
if self.n_fixed is not None:
|
|
29
|
+
if self.data.ndim == 2 and self.data.shape[1] == 1:
|
|
30
|
+
self.p_values = self.data.flatten()
|
|
31
|
+
else:
|
|
32
|
+
self.p_values = np.asarray(self.data).flatten()
|
|
33
|
+
self.n_values = np.full(len(self.p_values), self.n_fixed)
|
|
34
|
+
else:
|
|
35
|
+
if self.data.ndim == 2 and self.data.shape[1] == 2:
|
|
36
|
+
self.p_values = self.data[:, 0] / self.data[:, 1]
|
|
37
|
+
self.n_values = self.data[:, 1]
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError("При переменном объеме выборки data должно быть 2D массивом: [x, n]")
|
|
40
|
+
|
|
41
|
+
base_p = self.p_values[baseline_mask]
|
|
42
|
+
base_n = self.n_values[baseline_mask]
|
|
43
|
+
|
|
44
|
+
if self.target is not None:
|
|
45
|
+
self.p_bar = self.target
|
|
46
|
+
else:
|
|
47
|
+
total_defects = np.sum(base_p * base_n)
|
|
48
|
+
total_n = np.sum(base_n)
|
|
49
|
+
self.p_bar = total_defects / total_n if total_n > 0 else np.mean(base_p)
|
|
50
|
+
|
|
51
|
+
self.n_bar = np.mean(base_n)
|
|
52
|
+
|
|
53
|
+
if method == 'classic':
|
|
54
|
+
self.cl_main = self.p_bar
|
|
55
|
+
|
|
56
|
+
# --- ИЗМЕНЕНИЕ ЗДЕСЬ ---
|
|
57
|
+
# Если n_fixed задан ИЛИ пользователь попросил усредненные границы
|
|
58
|
+
if self.n_fixed is not None or use_average_n:
|
|
59
|
+
n_eval = self.n_fixed if self.n_fixed is not None else self.n_bar
|
|
60
|
+
|
|
61
|
+
sigma_p = np.sqrt(self.p_bar * (1 - self.p_bar) / n_eval)
|
|
62
|
+
self.ucl_main = self.p_bar + 3 * sigma_p
|
|
63
|
+
self.lcl_main = max(0, self.p_bar - 3 * sigma_p)
|
|
64
|
+
else:
|
|
65
|
+
# Переменные (ступенчатые) границы
|
|
66
|
+
self.ucl_main = np.zeros(len(self.p_values))
|
|
67
|
+
self.lcl_main = np.zeros(len(self.p_values))
|
|
68
|
+
|
|
69
|
+
for i in range(len(self.p_values)):
|
|
70
|
+
n_i = self.n_values[i]
|
|
71
|
+
sigma_p_i = np.sqrt(self.p_bar * (1 - self.p_bar) / n_i)
|
|
72
|
+
self.ucl_main[i] = self.p_bar + 3 * sigma_p_i
|
|
73
|
+
self.lcl_main[i] = max(0, self.p_bar - 3 * sigma_p_i)
|
|
74
|
+
|
|
75
|
+
self.sigma_est = np.sqrt(self.p_bar * (1 - self.p_bar))
|
|
76
|
+
|
|
77
|
+
elif method == 'percentiles':
|
|
78
|
+
self.cl_main = np.median(base_p)
|
|
79
|
+
self.ucl_main = np.percentile(base_p, 99.865)
|
|
80
|
+
self.lcl_main = np.percentile(base_p, 0.135)
|
|
81
|
+
self.sigma_est = np.std(base_p, ddof=1)
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError(f"Неизвестный метод: {method}")
|
|
85
|
+
|
|
86
|
+
self.stat_main = self.p_values
|
|
87
|
+
self.cl_disp = None
|
|
88
|
+
self.ucl_disp = None
|
|
89
|
+
self.lcl_disp = None
|
|
90
|
+
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
class CChart(BaseControlChart):
|
|
94
|
+
"""
|
|
95
|
+
c-карта (контрольная карта числа дефектов).
|
|
96
|
+
|
|
97
|
+
Используется для мониторинга количества дефектов на единицу продукции
|
|
98
|
+
при постоянном объеме выборки.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, data, datetimes=None, target=None, usl=None, lsl=None):
|
|
102
|
+
super().__init__(data, datetimes, target, usl, lsl)
|
|
103
|
+
self.main_label = "c-карта: Число дефектов"
|
|
104
|
+
self.disp_label = None
|
|
105
|
+
|
|
106
|
+
self.c_values = None
|
|
107
|
+
self.c_bar = None
|
|
108
|
+
|
|
109
|
+
def fit(self, baseline_mask=None, method='classic'):
|
|
110
|
+
if baseline_mask is None:
|
|
111
|
+
baseline_mask = np.ones(self.n_subgroups, dtype=bool)
|
|
112
|
+
|
|
113
|
+
if self.data.ndim == 2 and self.data.shape[1] == 1:
|
|
114
|
+
self.c_values = self.data.flatten()
|
|
115
|
+
else:
|
|
116
|
+
self.c_values = np.asarray(self.data).flatten()
|
|
117
|
+
|
|
118
|
+
base_c = self.c_values[baseline_mask]
|
|
119
|
+
|
|
120
|
+
if self.target is not None:
|
|
121
|
+
self.c_bar = self.target
|
|
122
|
+
else:
|
|
123
|
+
self.c_bar = np.mean(base_c)
|
|
124
|
+
|
|
125
|
+
if method == 'classic':
|
|
126
|
+
self.cl_main = self.c_bar
|
|
127
|
+
sigma_c = np.sqrt(self.c_bar)
|
|
128
|
+
self.ucl_main = self.c_bar + 3 * sigma_c
|
|
129
|
+
# LCL не может быть меньше 0
|
|
130
|
+
self.lcl_main = max(0, self.c_bar - 3 * sigma_c)
|
|
131
|
+
self.sigma_est = sigma_c
|
|
132
|
+
|
|
133
|
+
elif method == 'percentiles':
|
|
134
|
+
self.cl_main = np.median(base_c)
|
|
135
|
+
self.ucl_main = np.percentile(base_c, 99.865)
|
|
136
|
+
self.lcl_main = np.percentile(base_c, 0.135)
|
|
137
|
+
self.sigma_est = np.std(base_c, ddof=1)
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(f"Неизвестный метод: {method}")
|
|
141
|
+
|
|
142
|
+
self.stat_main = self.c_values
|
|
143
|
+
self.cl_disp = None
|
|
144
|
+
self.ucl_disp = None
|
|
145
|
+
self.lcl_disp = None
|
|
146
|
+
|
|
147
|
+
return self
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import warnings
|
|
3
|
+
from src.spc_lib.core.base_chart import BaseControlChart
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CUSUMChart(BaseControlChart):
|
|
7
|
+
"""
|
|
8
|
+
Кумулятивная сумма (CUSUM) - контрольная карта для обнаружения
|
|
9
|
+
малых сдвигов процесса.
|
|
10
|
+
|
|
11
|
+
Параметры:
|
|
12
|
+
----------
|
|
13
|
+
data : array-like
|
|
14
|
+
Данные подгрупп (2D массив) или индивидуальные значения (1D)
|
|
15
|
+
datetimes : array-like, optional
|
|
16
|
+
Временные метки
|
|
17
|
+
target : float, optional
|
|
18
|
+
Целевое значение процесса
|
|
19
|
+
usl, lsl : float, optional
|
|
20
|
+
Верхняя/нижняя границы спецификации
|
|
21
|
+
h : float, default=5
|
|
22
|
+
Параметр принятия решения (расстояние от нулевой линии до границы)
|
|
23
|
+
k : float, default=0.5
|
|
24
|
+
Параметр ссылочного значения (обычно 0.5 для стандартного CUSUM)
|
|
25
|
+
std_est : float, optional
|
|
26
|
+
Оценка стандартного отклонения. Если None, рассчитывается из данных.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, data, datetimes=None, target=None, usl=None, lsl=None,
|
|
30
|
+
h=5.0, k=0.5, std_est=None):
|
|
31
|
+
super().__init__(data, datetimes, target, usl, lsl)
|
|
32
|
+
self.h = h
|
|
33
|
+
self.k = k
|
|
34
|
+
self.std_est = std_est
|
|
35
|
+
self.main_label = "CUSUM: Кумулятивная сумма (верхняя/нижняя)"
|
|
36
|
+
self.disp_label = None
|
|
37
|
+
|
|
38
|
+
self.cusum_upper = None
|
|
39
|
+
self.cusum_lower = None
|
|
40
|
+
self.sigma_est = None
|
|
41
|
+
|
|
42
|
+
def fit(self, baseline_mask=None, method='classic'):
|
|
43
|
+
"""
|
|
44
|
+
Методы:
|
|
45
|
+
- 'classic': стандартный CUSUM с параметрами h и k
|
|
46
|
+
- 'percentiles': эмпирические пределы на основе процентилей
|
|
47
|
+
- 'made': робастный CUSUM на основе MAD
|
|
48
|
+
"""
|
|
49
|
+
if baseline_mask is None:
|
|
50
|
+
baseline_mask = np.ones(self.n_subgroups, dtype=bool)
|
|
51
|
+
|
|
52
|
+
if self.data.ndim == 2:
|
|
53
|
+
if self.subgroup_size == 1:
|
|
54
|
+
x = self.data.flatten()
|
|
55
|
+
n = 1
|
|
56
|
+
else:
|
|
57
|
+
x = np.mean(self.data, axis=1)
|
|
58
|
+
n = self.subgroup_size
|
|
59
|
+
else:
|
|
60
|
+
warnings.warn(
|
|
61
|
+
"Передан 1D массив. Рекомендуется использовать 2D формат: data.reshape(-1, 1)",
|
|
62
|
+
UserWarning
|
|
63
|
+
)
|
|
64
|
+
x = self.data
|
|
65
|
+
n = 1
|
|
66
|
+
|
|
67
|
+
base_x = x[baseline_mask]
|
|
68
|
+
|
|
69
|
+
if self.std_est is None:
|
|
70
|
+
if n > 1 and method == 'classic':
|
|
71
|
+
r = np.ptp(self.data[baseline_mask], axis=1)
|
|
72
|
+
d2 = {2: 1.128, 3: 1.693, 4: 2.059, 5: 2.326,
|
|
73
|
+
6: 2.534, 7: 2.704, 8: 2.847, 9: 2.970, 10: 3.078}
|
|
74
|
+
sigma_est = np.mean(r) / d2.get(n, 1.128)
|
|
75
|
+
else:
|
|
76
|
+
sigma_est = np.std(base_x, ddof=1)
|
|
77
|
+
else:
|
|
78
|
+
sigma_est = self.std_est
|
|
79
|
+
|
|
80
|
+
if self.target is not None:
|
|
81
|
+
target = self.target
|
|
82
|
+
elif method == 'classic':
|
|
83
|
+
target = np.mean(base_x)
|
|
84
|
+
else:
|
|
85
|
+
target = np.median(base_x)
|
|
86
|
+
|
|
87
|
+
# Нормировка
|
|
88
|
+
if sigma_est > 0:
|
|
89
|
+
z = (x - target) / sigma_est
|
|
90
|
+
else:
|
|
91
|
+
z = x - target
|
|
92
|
+
warnings.warn("Сигма равна 0, используется ненормированная статистика", UserWarning)
|
|
93
|
+
|
|
94
|
+
self.cusum_upper = np.zeros(len(x))
|
|
95
|
+
self.cusum_lower = np.zeros(len(x))
|
|
96
|
+
|
|
97
|
+
if method == 'classic':
|
|
98
|
+
for i in range(1, len(x)):
|
|
99
|
+
self.cusum_upper[i] = max(0, self.cusum_upper[i-1] + z[i] - self.k)
|
|
100
|
+
self.cusum_lower[i] = max(0, self.cusum_lower[i-1] - z[i] - self.k)
|
|
101
|
+
|
|
102
|
+
self.cl_main = 0
|
|
103
|
+
self.ucl_main = self.h
|
|
104
|
+
self.lcl_main = self.h
|
|
105
|
+
|
|
106
|
+
elif method == 'percentiles':
|
|
107
|
+
for i in range(1, len(x)):
|
|
108
|
+
self.cusum_upper[i] = max(0, self.cusum_upper[i-1] + z[i])
|
|
109
|
+
self.cusum_lower[i] = max(0, self.cusum_lower[i-1] - z[i])
|
|
110
|
+
|
|
111
|
+
base_cusum_upper = self.cusum_upper[baseline_mask]
|
|
112
|
+
base_cusum_lower = self.cusum_lower[baseline_mask]
|
|
113
|
+
|
|
114
|
+
self.cl_main = 0
|
|
115
|
+
self.ucl_main = np.percentile(base_cusum_upper, 99.865)
|
|
116
|
+
self.lcl_main = np.percentile(base_cusum_lower, 99.865)
|
|
117
|
+
|
|
118
|
+
elif method == 'made':
|
|
119
|
+
mad = np.median(np.abs(base_x - np.median(base_x)))
|
|
120
|
+
robust_sigma = 1.4826 * mad if mad > 0 else 1e-10
|
|
121
|
+
|
|
122
|
+
z_robust = (x - target) / robust_sigma if robust_sigma > 0 else x - target
|
|
123
|
+
|
|
124
|
+
for i in range(1, len(x)):
|
|
125
|
+
self.cusum_upper[i] = max(0, self.cusum_upper[i-1] + z_robust[i] - self.k)
|
|
126
|
+
self.cusum_lower[i] = max(0, self.cusum_lower[i-1] - z_robust[i] - self.k)
|
|
127
|
+
|
|
128
|
+
self.cl_main = 0
|
|
129
|
+
self.ucl_main = self.h
|
|
130
|
+
self.lcl_main = self.h
|
|
131
|
+
sigma_est = robust_sigma
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError(f"Неизвестный метод: {method}")
|
|
135
|
+
|
|
136
|
+
self.target = target
|
|
137
|
+
self.sigma_est = sigma_est
|
|
138
|
+
self.stat_main = x
|
|
139
|
+
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def get_cusum_stats(self):
|
|
143
|
+
"""Возвращает верхнюю и нижнюю CUSUM статистики"""
|
|
144
|
+
return self.cusum_upper, self.cusum_lower
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EWMAChart(BaseControlChart):
|
|
148
|
+
"""
|
|
149
|
+
Экспоненциально взвешенное скользящее среднее (EWMA)
|
|
150
|
+
- контрольная карта для обнаружения малых сдвигов процесса.
|
|
151
|
+
|
|
152
|
+
Параметры:
|
|
153
|
+
----------
|
|
154
|
+
data : array-like
|
|
155
|
+
Данные подгрупп (2D массив) или индивидуальные значения (1D)
|
|
156
|
+
datetimes : array-like, optional
|
|
157
|
+
Временные метки
|
|
158
|
+
target : float, optional
|
|
159
|
+
Целевое значение процесса
|
|
160
|
+
usl, lsl : float, optional
|
|
161
|
+
Верхняя/нижняя границы спецификации
|
|
162
|
+
lambda_ : float, default=0.2
|
|
163
|
+
Весовой коэффициент (0 < lambda <= 1)
|
|
164
|
+
L : float, default=3
|
|
165
|
+
Коэффициент ширины контрольных границ
|
|
166
|
+
std_est : float, optional
|
|
167
|
+
Оценка стандартного отклонения. Если None, рассчитывается из данных.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, data, datetimes=None, target=None, usl=None, lsl=None,
|
|
171
|
+
lambda_=0.2, L=3, std_est=None):
|
|
172
|
+
super().__init__(data, datetimes, target, usl, lsl)
|
|
173
|
+
self.lambda_ = lambda_
|
|
174
|
+
self.L = L
|
|
175
|
+
self.std_est = std_est
|
|
176
|
+
self.main_label = "EWMA: Экспоненциально взвешенное скользящее среднее"
|
|
177
|
+
self.disp_label = None
|
|
178
|
+
|
|
179
|
+
self.ewma_values = None
|
|
180
|
+
self.ewma_sigma = None
|
|
181
|
+
self.sigma_est = None
|
|
182
|
+
|
|
183
|
+
def fit(self, baseline_mask=None, method='classic'):
|
|
184
|
+
"""
|
|
185
|
+
Методы:
|
|
186
|
+
- 'classic': стандартная EWMA с фиксированными пределами
|
|
187
|
+
- 'percentiles': эмпирические пределы на основе процентилей
|
|
188
|
+
- 'made': робастная EWMA на основе MAD
|
|
189
|
+
"""
|
|
190
|
+
if baseline_mask is None:
|
|
191
|
+
baseline_mask = np.ones(self.n_subgroups, dtype=bool)
|
|
192
|
+
|
|
193
|
+
# Извлечение данных
|
|
194
|
+
if self.data.ndim == 2:
|
|
195
|
+
if self.subgroup_size == 1:
|
|
196
|
+
x = self.data.flatten()
|
|
197
|
+
n = 1
|
|
198
|
+
else:
|
|
199
|
+
x = np.mean(self.data, axis=1)
|
|
200
|
+
n = self.subgroup_size
|
|
201
|
+
else:
|
|
202
|
+
warnings.warn(
|
|
203
|
+
"Передан 1D массив. Рекомендуется использовать 2D формат: data.reshape(-1, 1)",
|
|
204
|
+
UserWarning
|
|
205
|
+
)
|
|
206
|
+
x = self.data
|
|
207
|
+
n = 1
|
|
208
|
+
|
|
209
|
+
base_x = x[baseline_mask]
|
|
210
|
+
|
|
211
|
+
# Оценка стандартного отклонения
|
|
212
|
+
if self.std_est is None:
|
|
213
|
+
if n > 1 and method == 'classic':
|
|
214
|
+
r = np.ptp(self.data[baseline_mask], axis=1)
|
|
215
|
+
d2 = {2: 1.128, 3: 1.693, 4: 2.059, 5: 2.326,
|
|
216
|
+
6: 2.534, 7: 2.704, 8: 2.847, 9: 2.970, 10: 3.078}
|
|
217
|
+
sigma_est = np.mean(r) / d2.get(n, 1.128)
|
|
218
|
+
else:
|
|
219
|
+
sigma_est = np.std(base_x, ddof=1)
|
|
220
|
+
else:
|
|
221
|
+
sigma_est = self.std_est
|
|
222
|
+
|
|
223
|
+
# Целевое значение
|
|
224
|
+
if self.target is not None:
|
|
225
|
+
target = self.target
|
|
226
|
+
elif method == 'classic':
|
|
227
|
+
target = np.mean(base_x)
|
|
228
|
+
else:
|
|
229
|
+
target = np.median(base_x)
|
|
230
|
+
|
|
231
|
+
lambda_ = self.lambda_
|
|
232
|
+
L = self.L
|
|
233
|
+
|
|
234
|
+
# Расчет EWMA
|
|
235
|
+
self.ewma_values = np.zeros(len(x))
|
|
236
|
+
self.ewma_values[0] = target
|
|
237
|
+
|
|
238
|
+
for i in range(1, len(x)):
|
|
239
|
+
self.ewma_values[i] = lambda_ * x[i] + (1 - lambda_) * self.ewma_values[i-1]
|
|
240
|
+
|
|
241
|
+
# Стандартное отклонение EWMA: σ * sqrt(λ/(2-λ) * (1 - (1-λ)^(2i)))
|
|
242
|
+
weights = np.zeros(len(x))
|
|
243
|
+
for i in range(1, len(x) + 1):
|
|
244
|
+
weights[i-1] = np.sqrt(lambda_ / (2 - lambda_) * (1 - (1 - lambda_)**(2*i)))
|
|
245
|
+
|
|
246
|
+
self.ewma_sigma = sigma_est * weights
|
|
247
|
+
|
|
248
|
+
# Расчет границ в зависимости от метода
|
|
249
|
+
if method == 'classic':
|
|
250
|
+
self.cl_main = target
|
|
251
|
+
self.ucl_main = target + L * self.ewma_sigma
|
|
252
|
+
self.lcl_main = target - L * self.ewma_sigma
|
|
253
|
+
|
|
254
|
+
elif method == 'percentiles':
|
|
255
|
+
base_ewma = self.ewma_values[baseline_mask]
|
|
256
|
+
self.cl_main = np.median(base_ewma)
|
|
257
|
+
self.ucl_main = np.percentile(base_ewma, 99.865)
|
|
258
|
+
self.lcl_main = np.percentile(base_ewma, 0.135)
|
|
259
|
+
|
|
260
|
+
elif method == 'made':
|
|
261
|
+
mad = np.median(np.abs(base_x - np.median(base_x)))
|
|
262
|
+
robust_sigma = 1.4826 * mad if mad > 0 else 1e-10
|
|
263
|
+
|
|
264
|
+
self.ewma_sigma = robust_sigma * weights
|
|
265
|
+
self.cl_main = target
|
|
266
|
+
self.ucl_main = target + L * self.ewma_sigma
|
|
267
|
+
self.lcl_main = target - L * self.ewma_sigma
|
|
268
|
+
sigma_est = robust_sigma
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
raise ValueError(f"Неизвестный метод: {method}")
|
|
272
|
+
|
|
273
|
+
self.target = target
|
|
274
|
+
self.sigma_est = sigma_est
|
|
275
|
+
self.stat_main = self.ewma_values
|
|
276
|
+
|
|
277
|
+
return self
|
|
278
|
+
|
|
279
|
+
def get_ewma_values(self):
|
|
280
|
+
"""Возвращает EWMA значения и их стандартные отклонения"""
|
|
281
|
+
return self.ewma_values, self.ewma_sigma
|
|
282
|
+
|
|
283
|
+
class CUSUMVarianceChart(BaseControlChart):
|
|
284
|
+
"""
|
|
285
|
+
CUSUM карта для контроля дисперсии/стандартного отклонения.
|
|
286
|
+
Использует преобразование Хокинса для мониторинга вариабельности процесса.
|
|
287
|
+
|
|
288
|
+
Параметры:
|
|
289
|
+
----------
|
|
290
|
+
data : array-like
|
|
291
|
+
Данные подгрупп (2D массив) или индивидуальные значения (1D)
|
|
292
|
+
datetimes : array-like, optional
|
|
293
|
+
Временные метки
|
|
294
|
+
target_mean : float, optional
|
|
295
|
+
Целевое среднее процесса. Если None - оценивается из данных.
|
|
296
|
+
target_std : float, optional
|
|
297
|
+
Целевое стандартное отклонение. Если None - оценивается из данных.
|
|
298
|
+
usl, lsl : float, optional
|
|
299
|
+
Верхняя/нижняя границы спецификации
|
|
300
|
+
h : float, default=5
|
|
301
|
+
Параметр принятия решения
|
|
302
|
+
k : float, default=0.5
|
|
303
|
+
Параметр ссылочного значения
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __init__(self, data, datetimes=None, target_mean=None, target_std=None,
|
|
307
|
+
usl=None, lsl=None, h=5.0, k=0.5):
|
|
308
|
+
super().__init__(data, datetimes, None, usl, lsl) # target передаем как None, используем target_mean
|
|
309
|
+
self.target_mean = target_mean
|
|
310
|
+
self.target_std = target_std
|
|
311
|
+
self.h = h
|
|
312
|
+
self.k = k
|
|
313
|
+
self.main_label = "CUSUM для дисперсии (верхняя/нижняя)"
|
|
314
|
+
self.disp_label = None
|
|
315
|
+
|
|
316
|
+
self.cusum_upper = None
|
|
317
|
+
self.cusum_lower = None
|
|
318
|
+
self.v_values = None # Преобразованные значения v_i
|
|
319
|
+
self.sigma_est = None
|
|
320
|
+
|
|
321
|
+
def fit(self, baseline_mask=None, method='classic'):
|
|
322
|
+
"""
|
|
323
|
+
Методы:
|
|
324
|
+
- 'classic': стандартный CUSUM для дисперсии с преобразованием Хокинса
|
|
325
|
+
- 'percentiles': эмпирические пределы на основе процентилей
|
|
326
|
+
"""
|
|
327
|
+
if baseline_mask is None:
|
|
328
|
+
baseline_mask = np.ones(self.n_subgroups, dtype=bool)
|
|
329
|
+
|
|
330
|
+
# Извлечение данных
|
|
331
|
+
if self.data.ndim == 2:
|
|
332
|
+
if self.subgroup_size == 1:
|
|
333
|
+
x = self.data.flatten()
|
|
334
|
+
n = 1
|
|
335
|
+
else:
|
|
336
|
+
x = np.mean(self.data, axis=1)
|
|
337
|
+
n = self.subgroup_size
|
|
338
|
+
else:
|
|
339
|
+
warnings.warn(
|
|
340
|
+
"Передан 1D массив. Рекомендуется использовать 2D формат: data.reshape(-1, 1)",
|
|
341
|
+
UserWarning
|
|
342
|
+
)
|
|
343
|
+
x = self.data
|
|
344
|
+
n = 1
|
|
345
|
+
|
|
346
|
+
base_x = x[baseline_mask]
|
|
347
|
+
|
|
348
|
+
# Оценка среднего и стандартного отклонения
|
|
349
|
+
if self.target_mean is not None:
|
|
350
|
+
mu = self.target_mean
|
|
351
|
+
elif method == 'classic':
|
|
352
|
+
mu = np.mean(base_x)
|
|
353
|
+
else:
|
|
354
|
+
mu = np.median(base_x)
|
|
355
|
+
|
|
356
|
+
if self.target_std is not None:
|
|
357
|
+
sigma = self.target_std
|
|
358
|
+
elif method == 'classic' and n > 1:
|
|
359
|
+
r = np.ptp(self.data[baseline_mask], axis=1)
|
|
360
|
+
d2 = {2: 1.128, 3: 1.693, 4: 2.059, 5: 2.326,
|
|
361
|
+
6: 2.534, 7: 2.704, 8: 2.847, 9: 2.970, 10: 3.078}
|
|
362
|
+
sigma = np.mean(r) / d2.get(n, 1.128)
|
|
363
|
+
else:
|
|
364
|
+
sigma = np.std(base_x, ddof=1)
|
|
365
|
+
|
|
366
|
+
# Стандартизованные значения
|
|
367
|
+
if sigma > 0:
|
|
368
|
+
y = (x - mu) / sigma
|
|
369
|
+
else:
|
|
370
|
+
y = x - mu
|
|
371
|
+
warnings.warn("Сигма равна 0, используется ненормированная статистика", UserWarning)
|
|
372
|
+
|
|
373
|
+
# Преобразование Хокинса для мониторинга дисперсии
|
|
374
|
+
# v_i = (sqrt(|y_i|) - 0.822) / 0.349
|
|
375
|
+
# При нормальном распределении v_i ~ N(0,1)
|
|
376
|
+
self.v_values = (np.sqrt(np.abs(y)) - 0.822) / 0.349
|
|
377
|
+
|
|
378
|
+
self.cusum_upper = np.zeros(len(x))
|
|
379
|
+
self.cusum_lower = np.zeros(len(x))
|
|
380
|
+
|
|
381
|
+
if method == 'classic':
|
|
382
|
+
# Стандартный CUSUM для v_i
|
|
383
|
+
for i in range(1, len(x)):
|
|
384
|
+
self.cusum_upper[i] = max(0, self.cusum_upper[i-1] + self.v_values[i] - self.k)
|
|
385
|
+
self.cusum_lower[i] = max(0, self.cusum_lower[i-1] - self.k - self.v_values[i])
|
|
386
|
+
|
|
387
|
+
self.cl_main = 0
|
|
388
|
+
self.ucl_main = self.h
|
|
389
|
+
self.lcl_main = self.h
|
|
390
|
+
|
|
391
|
+
elif method == 'percentiles':
|
|
392
|
+
for i in range(1, len(x)):
|
|
393
|
+
self.cusum_upper[i] = max(0, self.cusum_upper[i-1] + self.v_values[i])
|
|
394
|
+
self.cusum_lower[i] = max(0, self.cusum_lower[i-1] - self.v_values[i])
|
|
395
|
+
|
|
396
|
+
base_cusum_upper = self.cusum_upper[baseline_mask]
|
|
397
|
+
base_cusum_lower = self.cusum_lower[baseline_mask]
|
|
398
|
+
|
|
399
|
+
self.cl_main = 0
|
|
400
|
+
self.ucl_main = np.percentile(base_cusum_upper, 99.865)
|
|
401
|
+
self.lcl_main = np.percentile(base_cusum_lower, 99.865)
|
|
402
|
+
|
|
403
|
+
else:
|
|
404
|
+
raise ValueError(f"Неизвестный метод: {method}")
|
|
405
|
+
|
|
406
|
+
self.target = mu # Для совместимости с базовым классом
|
|
407
|
+
self.target_mean = mu
|
|
408
|
+
self.target_std = sigma
|
|
409
|
+
self.sigma_est = sigma
|
|
410
|
+
self.stat_main = x # Исходные данные для справки
|
|
411
|
+
|
|
412
|
+
return self
|