Yoon-gu Hwang Claude commited on
Commit
befeb85
ยท
1 Parent(s): 93ec097

Change to continuous data with line graph visualization

Browse files

- Generate continuous time-series data with sine wave patterns
- Show actual data points using line graphs
- Add drift point markers with red dashed lines
- Update analyzer to work with continuous data

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. analyzer.py +28 -56
  2. drift_simulator.py +31 -30
  3. visualizer.py +54 -108
analyzer.py CHANGED
@@ -11,21 +11,11 @@ def analyze_drift(X: np.ndarray, y: np.ndarray, drift_points: np.ndarray, drift_
11
  "drift_locations": drift_points.tolist() if len(drift_points) > 0 else [],
12
  }
13
 
14
- # incremental drift๋Š” ์—ฐ์† ๊ฐ’, ๋‚˜๋จธ์ง€๋Š” ์ด์ง„ ๋ถ„๋ฅ˜
15
- if drift_type == "incremental":
16
- # ์—ฐ์† ๊ฐ’ ๋ถ„์„
17
- analysis["mean_y"] = float(np.mean(y))
18
- analysis["std_y"] = float(np.std(y))
19
- analysis["min_y"] = float(np.min(y))
20
- analysis["max_y"] = float(np.max(y))
21
- else:
22
- # ์ด์ง„ ๋ถ„๋ฅ˜ ๋ถ„์„
23
- class_0_count = int(np.sum(y == 0))
24
- class_1_count = int(np.sum(y == 1))
25
- analysis["class_0_count"] = class_0_count
26
- analysis["class_1_count"] = class_1_count
27
- analysis["class_0_ratio"] = float(class_0_count / len(y))
28
- analysis["class_1_ratio"] = float(class_1_count / len(y))
29
 
30
  # ์„ธ๊ทธ๋จผํŠธ๋ณ„ ๋ถ„์„
31
  segments = []
@@ -36,31 +26,26 @@ def analyze_drift(X: np.ndarray, y: np.ndarray, drift_points: np.ndarray, drift_
36
  end = segment_boundaries[i + 1]
37
 
38
  segment_y = y[start:end]
 
39
 
40
- if drift_type == "incremental":
41
- # ์—ฐ์† ๊ฐ’ ์„ธ๊ทธ๋จผํŠธ ๋ถ„์„
42
- segments.append({
43
- "segment_id": i,
44
- "start_idx": int(start),
45
- "end_idx": int(end),
46
- "mean": float(np.mean(segment_y)),
47
- "std": float(np.std(segment_y))
48
- })
49
  else:
50
- # ์ด์ง„ ๋ถ„๋ฅ˜ ์„ธ๊ทธ๋จผํŠธ ๋ถ„์„
51
- class_0_count = int(np.sum(segment_y == 0))
52
- class_1_count = int(np.sum(segment_y == 1))
53
- total = len(segment_y)
54
-
55
- segments.append({
56
- "segment_id": i,
57
- "start_idx": int(start),
58
- "end_idx": int(end),
59
- "class_0_count": class_0_count,
60
- "class_1_count": class_1_count,
61
- "class_0_ratio": float(class_0_count / total) if total > 0 else 0.0,
62
- "class_1_ratio": float(class_1_count / total) if total > 0 else 0.0
63
- })
64
 
65
  analysis["segments"] = segments
66
 
@@ -80,32 +65,19 @@ def format_analysis_summary(analysis: Dict) -> str:
80
  **์ „์ฒด ๋ฐ์ดํ„ฐ:**
81
  - ์ด ์ƒ˜ํ”Œ ์ˆ˜: {analysis['total_samples']}
82
  - ๋“œ๋ฆฌํ”„ํŠธ ๋ฐœ์ƒ ํšŸ์ˆ˜: {analysis['num_drift_points']}
83
- """
84
-
85
- if drift_type == "incremental":
86
- summary += f"""- ํ‰๊ท  ๊ฐ’: {analysis['mean_y']:.2f}
87
  - ํ‘œ์ค€ํŽธ์ฐจ: {analysis['std_y']:.2f}
88
  - ๋ฒ”์œ„: [{analysis['min_y']:.2f}, {analysis['max_y']:.2f}]
89
- """
90
- else:
91
- summary += f"""- Class 0 (ํŒŒ๋ž€์ƒ‰): {analysis['class_0_count']} ์ƒ˜ํ”Œ ({analysis['class_0_ratio']*100:.1f}%)
92
- - Class 1 (์ดˆ๋ก์ƒ‰): {analysis['class_1_count']} ์ƒ˜ํ”Œ ({analysis['class_1_ratio']*100:.1f}%)
93
- """
94
 
95
- summary += "\n**์„ธ๊ทธ๋จผํŠธ๋ณ„ ๋ถ„์„:**\n"
 
96
 
97
  for seg in analysis['segments']:
98
- if drift_type == "incremental":
99
- summary += f"""
100
  **์„ธ๊ทธ๋จผํŠธ {seg['segment_id'] + 1}** (์ƒ˜ํ”Œ {seg['start_idx']}-{seg['end_idx']})
101
  - ํ‰๊ท : {seg['mean']:.2f}
102
  - ํ‘œ์ค€ํŽธ์ฐจ: {seg['std']:.2f}
103
- """
104
- else:
105
- summary += f"""
106
- **์„ธ๊ทธ๋จผํŠธ {seg['segment_id'] + 1}** (์ƒ˜ํ”Œ {seg['start_idx']}-{seg['end_idx']})
107
- - Class 0: {seg['class_0_count']} ์ƒ˜ํ”Œ ({seg['class_0_ratio']*100:.1f}%)
108
- - Class 1: {seg['class_1_count']} ์ƒ˜ํ”Œ ({seg['class_1_ratio']*100:.1f}%)
109
  """
110
 
111
  return summary
 
11
  "drift_locations": drift_points.tolist() if len(drift_points) > 0 else [],
12
  }
13
 
14
+ # ์ „์ฒด ํ†ต๊ณ„ (๋ชจ๋“  drift type์„ ์—ฐ์† ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ)
15
+ analysis["mean_y"] = float(np.mean(y))
16
+ analysis["std_y"] = float(np.std(y))
17
+ analysis["min_y"] = float(np.min(y))
18
+ analysis["max_y"] = float(np.max(y))
 
 
 
 
 
 
 
 
 
 
19
 
20
  # ์„ธ๊ทธ๋จผํŠธ๋ณ„ ๋ถ„์„
21
  segments = []
 
26
  end = segment_boundaries[i + 1]
27
 
28
  segment_y = y[start:end]
29
+ segment_X = X[start:end]
30
 
31
+ # ์„ ํ˜• ํšŒ๊ท€ ๊ณ„์ˆ˜ ๊ณ„์‚ฐ
32
+ if len(segment_X) > 1:
33
+ coeffs = np.polyfit(segment_X, segment_y, 1)
34
+ slope = float(coeffs[0])
35
+ intercept = float(coeffs[1])
 
 
 
 
36
  else:
37
+ slope = 0.0
38
+ intercept = float(segment_y[0]) if len(segment_y) > 0 else 0.0
39
+
40
+ segments.append({
41
+ "segment_id": i,
42
+ "start_idx": int(start),
43
+ "end_idx": int(end),
44
+ "mean": float(np.mean(segment_y)),
45
+ "std": float(np.std(segment_y)),
46
+ "slope": slope,
47
+ "intercept": intercept
48
+ })
 
 
49
 
50
  analysis["segments"] = segments
51
 
 
65
  **์ „์ฒด ๋ฐ์ดํ„ฐ:**
66
  - ์ด ์ƒ˜ํ”Œ ์ˆ˜: {analysis['total_samples']}
67
  - ๋“œ๋ฆฌํ”„ํŠธ ๋ฐœ์ƒ ํšŸ์ˆ˜: {analysis['num_drift_points']}
68
+ - ํ‰๊ท  ๊ฐ’: {analysis['mean_y']:.2f}
 
 
 
69
  - ํ‘œ์ค€ํŽธ์ฐจ: {analysis['std_y']:.2f}
70
  - ๋ฒ”์œ„: [{analysis['min_y']:.2f}, {analysis['max_y']:.2f}]
 
 
 
 
 
71
 
72
+ **์„ธ๊ทธ๋จผํŠธ๋ณ„ ๋ถ„์„:**
73
+ """
74
 
75
  for seg in analysis['segments']:
76
+ summary += f"""
 
77
  **์„ธ๊ทธ๋จผํŠธ {seg['segment_id'] + 1}** (์ƒ˜ํ”Œ {seg['start_idx']}-{seg['end_idx']})
78
  - ํ‰๊ท : {seg['mean']:.2f}
79
  - ํ‘œ์ค€ํŽธ์ฐจ: {seg['std']:.2f}
80
+ - ํŠธ๋ Œ๋“œ: y = {seg['slope']:.4f}x + {seg['intercept']:.2f}
 
 
 
 
 
81
  """
82
 
83
  return summary
drift_simulator.py CHANGED
@@ -3,14 +3,14 @@ from typing import Tuple
3
 
4
  def generate_sudden_drift(n_samples: int = 1000, drift_point: int = 500) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
5
  """๊ธ‰๊ฒฉํ•œ ๋“œ๋ฆฌํ”„ํŠธ: t ์‹œ์ ์—์„œ ๊ฐ‘์ž๊ธฐ ๋ฐ์ดํ„ฐ ๋ถ„ํฌ ๋ณ€๊ฒฝ"""
6
- X = np.arange(n_samples) # ์‹œ๊ฐ„ ์ธ๋ฑ์Šค
7
- y = np.zeros(n_samples, dtype=int)
8
 
9
- # Before drift: class 0 (ํŒŒ๋ž€์ƒ‰)
10
- y[:drift_point] = 0
11
 
12
- # After drift: class 1 (์ดˆ๋ก์ƒ‰)
13
- y[drift_point:] = 1
14
 
15
  drift_points = np.array([drift_point])
16
  return X, y, drift_points
@@ -18,41 +18,42 @@ def generate_sudden_drift(n_samples: int = 1000, drift_point: int = 500) -> Tupl
18
 
19
  def generate_gradual_drift(n_samples: int = 1000, drift_start: int = 300, drift_end: int = 700) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
20
  """์ ์ง„์  ๋“œ๋ฆฌํ”„ํŠธ: ๋‘ ๋ถ„ํฌ๊ฐ€ ์„ž์ด๋ฉฐ ์ฒœ์ฒœํžˆ ์ „ํ™˜"""
21
- X = np.arange(n_samples) # ์‹œ๊ฐ„ ์ธ๋ฑ์Šค
22
- y = np.zeros(n_samples, dtype=int)
23
 
24
- # Before drift: class 0 (ํŒŒ๋ž€์ƒ‰)
25
- y[:drift_start] = 0
26
 
27
- # Gradual transition: class 0๊ณผ class 1์ด ์„ž์ž„
28
  transition_length = drift_end - drift_start
29
  for i in range(drift_start, drift_end):
30
- # ์ ์ง„์ ์œผ๋กœ class 1์˜ ๋น„์œจ ์ฆ๊ฐ€
31
  weight = (i - drift_start) / transition_length
32
- y[i] = 1 if np.random.random() < weight else 0
 
 
33
 
34
- # After drift: class 1 (์ดˆ๋ก์ƒ‰)
35
- y[drift_end:] = 1
36
 
37
  drift_points = np.array([drift_start, drift_end])
38
  return X, y, drift_points
39
 
40
 
41
- def generate_incremental_drift(n_samples: int = 1000, n_steps: int = 10) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
42
  """์ฆ๋ถ„์  ๋“œ๋ฆฌํ”„ํŠธ: ๊ณ„๋‹จ์‹์œผ๋กœ ์ž‘์€ ๋ณ€ํ™”๊ฐ€ ๋ˆ„์ """
43
- X = np.arange(n_samples) # ์‹œ๊ฐ„ ์ธ๋ฑ์Šค
44
- y = np.zeros(n_samples) # ์—ฐ์† ๊ฐ’ (์‹œ๊ฐํ™”๋ฅผ ์œ„ํ•ด)
45
 
46
- step_size = n_samples // n_steps
47
  drift_points = []
48
 
49
- for step in range(n_steps):
50
  start_idx = step * step_size
51
- end_idx = (step + 1) * step_size if step < n_steps - 1 else n_samples
52
 
53
- # ๊ฐ ๋‹จ๊ณ„๋งˆ๋‹ค 0์—์„œ 1๋กœ ์ ์ง„์  ๋ณ€ํ™”
54
- value = step / (n_steps - 1)
55
- y[start_idx:end_idx] = value
56
 
57
  if step > 0:
58
  drift_points.append(start_idx)
@@ -62,8 +63,8 @@ def generate_incremental_drift(n_samples: int = 1000, n_steps: int = 10) -> Tupl
62
 
63
  def generate_recurring_drift(n_samples: int = 1000, cycle_length: int = 250) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
64
  """๋ฐ˜๋ณต์  ๋“œ๋ฆฌํ”„ํŠธ: ์ด์ „ ๋ถ„ํฌ๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ ์žฌ๋“ฑ์žฅ"""
65
- X = np.arange(n_samples) # ์‹œ๊ฐ„ ์ธ๋ฑ์Šค
66
- y = np.zeros(n_samples, dtype=int)
67
 
68
  drift_points = []
69
 
@@ -71,11 +72,11 @@ def generate_recurring_drift(n_samples: int = 1000, cycle_length: int = 250) ->
71
  cycle_pos = i % cycle_length
72
 
73
  if cycle_pos < cycle_length // 2:
74
- # Concept A: class 0 (ํŒŒ๋ž€์ƒ‰)
75
- y[i] = 0
76
  else:
77
- # Concept B: class 1 (์ดˆ๋ก์ƒ‰)
78
- y[i] = 1
79
 
80
  if cycle_pos == cycle_length // 2:
81
  drift_points.append(i)
 
3
 
4
  def generate_sudden_drift(n_samples: int = 1000, drift_point: int = 500) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
5
  """๊ธ‰๊ฒฉํ•œ ๋“œ๋ฆฌํ”„ํŠธ: t ์‹œ์ ์—์„œ ๊ฐ‘์ž๊ธฐ ๋ฐ์ดํ„ฐ ๋ถ„ํฌ ๋ณ€๊ฒฝ"""
6
+ X = np.arange(n_samples)
7
+ y = np.zeros(n_samples)
8
 
9
+ # Before drift: y = 2 + sin(X/50) + noise
10
+ y[:drift_point] = 2 + np.sin(X[:drift_point] / 50) + np.random.normal(0, 0.3, drift_point)
11
 
12
+ # After drift: y = 5 - sin(X/50) + noise (์™„์ „ํžˆ ๋‹ค๋ฅธ ํŒจํ„ด)
13
+ y[drift_point:] = 5 - np.sin(X[drift_point:] / 50) + np.random.normal(0, 0.3, n_samples - drift_point)
14
 
15
  drift_points = np.array([drift_point])
16
  return X, y, drift_points
 
18
 
19
  def generate_gradual_drift(n_samples: int = 1000, drift_start: int = 300, drift_end: int = 700) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
20
  """์ ์ง„์  ๋“œ๋ฆฌํ”„ํŠธ: ๋‘ ๋ถ„ํฌ๊ฐ€ ์„ž์ด๋ฉฐ ์ฒœ์ฒœํžˆ ์ „ํ™˜"""
21
+ X = np.arange(n_samples)
22
+ y = np.zeros(n_samples)
23
 
24
+ # Before drift: y = 2 + sin(X/50) + noise
25
+ y[:drift_start] = 2 + np.sin(X[:drift_start] / 50) + np.random.normal(0, 0.3, drift_start)
26
 
27
+ # Gradual transition: ์ ์ง„์ ์œผ๋กœ ๋ณ€ํ™˜
28
  transition_length = drift_end - drift_start
29
  for i in range(drift_start, drift_end):
 
30
  weight = (i - drift_start) / transition_length
31
+ old_concept = 2 + np.sin(X[i] / 50) + np.random.normal(0, 0.3)
32
+ new_concept = 5 - np.sin(X[i] / 50) + np.random.normal(0, 0.3)
33
+ y[i] = (1 - weight) * old_concept + weight * new_concept
34
 
35
+ # After drift: y = 5 - sin(X/50) + noise
36
+ y[drift_end:] = 5 - np.sin(X[drift_end:] / 50) + np.random.normal(0, 0.3, n_samples - drift_end)
37
 
38
  drift_points = np.array([drift_start, drift_end])
39
  return X, y, drift_points
40
 
41
 
42
+ def generate_incremental_drift(n_samples: int = 1000, n_steps: int = 5) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
43
  """์ฆ๋ถ„์  ๋“œ๋ฆฌํ”„ํŠธ: ๊ณ„๋‹จ์‹์œผ๋กœ ์ž‘์€ ๋ณ€ํ™”๊ฐ€ ๋ˆ„์ """
44
+ X = np.arange(n_samples)
45
+ y = np.zeros(n_samples)
46
 
47
+ step_size = n_samples // (n_steps + 1)
48
  drift_points = []
49
 
50
+ for step in range(n_steps + 1):
51
  start_idx = step * step_size
52
+ end_idx = (step + 1) * step_size if step < n_steps else n_samples
53
 
54
+ # ๊ฐ ๋‹จ๊ณ„๋งˆ๋‹ค ํ‰๊ท ์ด ์กฐ๊ธˆ์”ฉ ๋ณ€ํ™”
55
+ mean_shift = 2 + (step / n_steps) * 3 # 2์—์„œ 5๋กœ ์ ์ง„์  ๋ณ€ํ™”
56
+ y[start_idx:end_idx] = mean_shift + np.sin(X[start_idx:end_idx] / 50) + np.random.normal(0, 0.3, end_idx - start_idx)
57
 
58
  if step > 0:
59
  drift_points.append(start_idx)
 
63
 
64
  def generate_recurring_drift(n_samples: int = 1000, cycle_length: int = 250) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
65
  """๋ฐ˜๋ณต์  ๋“œ๋ฆฌํ”„ํŠธ: ์ด์ „ ๋ถ„ํฌ๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ ์žฌ๋“ฑ์žฅ"""
66
+ X = np.arange(n_samples)
67
+ y = np.zeros(n_samples)
68
 
69
  drift_points = []
70
 
 
72
  cycle_pos = i % cycle_length
73
 
74
  if cycle_pos < cycle_length // 2:
75
+ # Concept A: y = 2 + sin(X/50) + noise
76
+ y[i] = 2 + np.sin(X[i] / 50) + np.random.normal(0, 0.3)
77
  else:
78
+ # Concept B: y = 5 - sin(X/50) + noise
79
+ y[i] = 5 - np.sin(X[i] / 50) + np.random.normal(0, 0.3)
80
 
81
  if cycle_pos == cycle_length // 2:
82
  drift_points.append(i)
visualizer.py CHANGED
@@ -7,59 +7,30 @@ def create_drift_visualization(X: np.ndarray, y: np.ndarray, drift_points: np.nd
7
 
8
  fig = go.Figure()
9
 
10
- # incremental drift๋Š” ์—ฐ์† ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ
11
- if drift_type == "incremental":
12
- # ์ƒ‰์ƒ ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ
13
- colors = []
14
- for val in y:
15
- # ํŒŒ๋ž€์ƒ‰(0)์—์„œ ์ดˆ๋ก์ƒ‰(1)๋กœ ์ ์ง„์  ๋ณ€ํ™”
16
- r = int(50 + 50 * val)
17
- g = int(100 + 79 * val)
18
- b = int(200 - 87 * val)
19
- colors.append(f'rgb({r}, {g}, {b})')
20
-
21
- # Scatter๋กœ ํ‘œํ˜„ (์ƒ‰์ƒ ์˜์—ญ)
22
- fig.add_trace(go.Scatter(
23
- x=X,
24
- y=np.ones(len(X)),
25
- mode='markers',
26
- marker=dict(
27
- color=colors,
28
- size=10,
29
- symbol='square',
30
- line=dict(width=0)
31
- ),
32
- showlegend=False,
33
- hovertemplate='Time: %{x}<br>Value: %{customdata:.2f}<extra></extra>',
34
- customdata=y
35
- ))
36
- else:
37
- # ์ด์ง„ ๋ถ„๋ฅ˜ (0: ํŒŒ๋ž€์ƒ‰, 1: ์ดˆ๋ก์ƒ‰)
38
- # Class 0 (ํŒŒ๋ž€์ƒ‰)
39
- class_0_mask = y == 0
40
- if np.any(class_0_mask):
41
- fig.add_trace(go.Scatter(
42
- x=X[class_0_mask],
43
- y=np.ones(np.sum(class_0_mask)),
44
- mode='markers',
45
- marker=dict(color='rgb(65, 105, 225)', size=10, symbol='square', line=dict(width=0)),
46
- name='Class 0',
47
- showlegend=True,
48
- hovertemplate='Time: %{x}<br>Class: 0<extra></extra>'
49
- ))
50
-
51
- # Class 1 (์ดˆ๋ก์ƒ‰)
52
- class_1_mask = y == 1
53
- if np.any(class_1_mask):
54
- fig.add_trace(go.Scatter(
55
- x=X[class_1_mask],
56
- y=np.ones(np.sum(class_1_mask)),
57
- mode='markers',
58
- marker=dict(color='rgb(50, 205, 50)', size=10, symbol='square', line=dict(width=0)),
59
- name='Class 1',
60
- showlegend=True,
61
- hovertemplate='Time: %{x}<br>Class: 1<extra></extra>'
62
- ))
63
 
64
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
65
  title_map = {
@@ -77,11 +48,11 @@ def create_drift_visualization(X: np.ndarray, y: np.ndarray, drift_points: np.nd
77
  font=dict(size=20)
78
  ),
79
  xaxis_title="Time",
80
- yaxis_title="Data distribution",
81
  hovermode='closest',
82
  template='plotly_white',
83
- height=400,
84
- showlegend=(drift_type != "incremental"),
85
  legend=dict(
86
  yanchor="top",
87
  y=0.99,
@@ -89,13 +60,14 @@ def create_drift_visualization(X: np.ndarray, y: np.ndarray, drift_points: np.nd
89
  x=0.99
90
  ),
91
  xaxis=dict(
92
- showgrid=False,
93
- showticklabels=True
 
94
  ),
95
  yaxis=dict(
96
- showgrid=False,
97
- showticklabels=False,
98
- range=[0.5, 1.5]
99
  ),
100
  plot_bgcolor='white'
101
  )
@@ -121,57 +93,31 @@ def create_comparison_visualization(drift_data_dict: dict) -> go.Figure:
121
  if drift_type in drift_data_dict:
122
  X, y, drift_points = drift_data_dict[drift_type]
123
 
124
- if drift_type == "incremental":
125
- # Incremental drift: ์—ฐ์† ์ƒ‰์ƒ ๋ณ€ํ™”
126
- colors = []
127
- for val in y:
128
- r = int(50 + 50 * val)
129
- g = int(100 + 79 * val)
130
- b = int(200 - 87 * val)
131
- colors.append(f'rgb({r}, {g}, {b})')
132
-
133
- fig.add_trace(
134
- go.Scatter(
135
- x=X,
136
- y=np.ones(len(X)),
137
- mode='markers',
138
- marker=dict(color=colors, size=5, symbol='square', line=dict(width=0)),
139
- showlegend=False
140
- ),
 
 
141
  row=row, col=col
142
  )
143
- else:
144
- # ์ด์ง„ ๋ถ„๋ฅ˜
145
- class_0_mask = y == 0
146
- class_1_mask = y == 1
147
-
148
- if np.any(class_0_mask):
149
- fig.add_trace(
150
- go.Scatter(
151
- x=X[class_0_mask],
152
- y=np.ones(np.sum(class_0_mask)),
153
- mode='markers',
154
- marker=dict(color='rgb(65, 105, 225)', size=5, symbol='square', line=dict(width=0)),
155
- showlegend=False
156
- ),
157
- row=row, col=col
158
- )
159
-
160
- if np.any(class_1_mask):
161
- fig.add_trace(
162
- go.Scatter(
163
- x=X[class_1_mask],
164
- y=np.ones(np.sum(class_1_mask)),
165
- mode='markers',
166
- marker=dict(color='rgb(50, 205, 50)', size=5, symbol='square', line=dict(width=0)),
167
- showlegend=False
168
- ),
169
- row=row, col=col
170
- )
171
 
172
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
173
- fig.update_xaxes(title_text="Time", showgrid=False, showticklabels=False)
174
- fig.update_yaxes(title_text="Data distribution", showgrid=False, showticklabels=False, range=[0.5, 1.5])
175
  fig.update_layout(
176
  height=800,
177
  title_text="Concept Drift Types Comparison",
 
7
 
8
  fig = go.Figure()
9
 
10
+ # ๋ฉ”์ธ ๋ผ์ธ ๊ทธ๋ž˜ํ”„
11
+ fig.add_trace(go.Scatter(
12
+ x=X,
13
+ y=y,
14
+ mode='lines+markers',
15
+ name='Data',
16
+ line=dict(color='rgb(100, 140, 200)', width=2),
17
+ marker=dict(size=4, color='rgb(100, 140, 200)'),
18
+ hovertemplate='Time: %{x}<br>Value: %{y:.2f}<extra></extra>'
19
+ ))
20
+
21
+ # ๋“œ๋ฆฌํ”„ํŠธ ๋ฐœ์ƒ ์ง€์  ํ‘œ์‹œ
22
+ y_min, y_max = y.min(), y.max()
23
+ y_range = y_max - y_min
24
+
25
+ for i, drift_point in enumerate(drift_points):
26
+ fig.add_vline(
27
+ x=X[drift_point],
28
+ line_dash="dash",
29
+ line_color="red",
30
+ line_width=2,
31
+ annotation_text=f"Drift {i+1}",
32
+ annotation_position="top"
33
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
36
  title_map = {
 
48
  font=dict(size=20)
49
  ),
50
  xaxis_title="Time",
51
+ yaxis_title="Value",
52
  hovermode='closest',
53
  template='plotly_white',
54
+ height=500,
55
+ showlegend=True,
56
  legend=dict(
57
  yanchor="top",
58
  y=0.99,
 
60
  x=0.99
61
  ),
62
  xaxis=dict(
63
+ showgrid=True,
64
+ gridwidth=1,
65
+ gridcolor='LightGray'
66
  ),
67
  yaxis=dict(
68
+ showgrid=True,
69
+ gridwidth=1,
70
+ gridcolor='LightGray'
71
  ),
72
  plot_bgcolor='white'
73
  )
 
93
  if drift_type in drift_data_dict:
94
  X, y, drift_points = drift_data_dict[drift_type]
95
 
96
+ # ๋ผ์ธ ๊ทธ๋ž˜ํ”„ ์ถ”๊ฐ€
97
+ fig.add_trace(
98
+ go.Scatter(
99
+ x=X,
100
+ y=y,
101
+ mode='lines',
102
+ line=dict(color='rgb(100, 140, 200)', width=1),
103
+ showlegend=False
104
+ ),
105
+ row=row, col=col
106
+ )
107
+
108
+ # ๋“œ๋ฆฌํ”„ํŠธ ์ง€์  ํ‘œ์‹œ
109
+ for drift_point in drift_points:
110
+ fig.add_vline(
111
+ x=X[drift_point],
112
+ line_dash="dash",
113
+ line_color="red",
114
+ line_width=1,
115
  row=row, col=col
116
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
119
+ fig.update_xaxes(title_text="Time", showgrid=True, gridcolor='LightGray')
120
+ fig.update_yaxes(title_text="Value", showgrid=True, gridcolor='LightGray')
121
  fig.update_layout(
122
  height=800,
123
  title_text="Concept Drift Types Comparison",