Yoon-gu Hwang Claude commited on
Commit
7ab1194
Β·
1 Parent(s): 3ab49ae

Change visualization to bar chart with binary classification

Browse files

- Update drift_simulator.py: Generate binary classification data (class 0 vs class 1)
- Update visualizer.py: Create bar chart visualization instead of scatter plot
- Update analyzer.py: Analyze binary classification data
- Add test files to .gitignore

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

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

Files changed (4) hide show
  1. .gitignore +2 -0
  2. analyzer.py +59 -29
  3. drift_simulator.py +30 -33
  4. visualizer.py +119 -68
.gitignore CHANGED
@@ -15,3 +15,5 @@ build/
15
  .vscode/
16
  *.log
17
  flagged/
 
 
 
15
  .vscode/
16
  *.log
17
  flagged/
18
+ *.html
19
+ test_*.py
analyzer.py CHANGED
@@ -11,11 +11,21 @@ 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
- # 전체 톡계
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,26 +36,31 @@ def analyze_drift(X: np.ndarray, y: np.ndarray, drift_points: np.ndarray, drift_
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
 
@@ -55,27 +70,42 @@ def analyze_drift(X: np.ndarray, y: np.ndarray, drift_points: np.ndarray, drift_
55
  def format_analysis_summary(analysis: Dict) -> str:
56
  """뢄석 κ²°κ³Όλ₯Ό μ‚¬λžŒμ΄ 읽기 μ‰¬μš΄ ν˜•μ‹μœΌλ‘œ 포맷"""
57
 
 
 
58
  summary = f"""
59
  ## λ“œλ¦¬ν”„νŠΈ 뢄석 κ²°κ³Ό
60
 
61
- **λ“œλ¦¬ν”„νŠΈ μœ ν˜•:** {analysis['drift_type'].upper()}
62
 
63
  **전체 데이터:**
64
  - 총 μƒ˜ν”Œ 수: {analysis['total_samples']}
65
  - λ“œλ¦¬ν”„νŠΈ λ°œμƒ 횟수: {analysis['num_drift_points']}
66
- - 평균: {analysis['mean_y']:.2f}
 
 
 
67
  - ν‘œμ€€νŽΈμ°¨: {analysis['std_y']:.2f}
68
  - λ²”μœ„: [{analysis['min_y']:.2f}, {analysis['max_y']:.2f}]
69
-
70
- **μ„Έκ·Έλ¨ΌνŠΈλ³„ 뢄석:**
 
 
71
  """
72
 
 
 
73
  for seg in analysis['segments']:
74
- summary += f"""
 
75
  **μ„Έκ·Έλ¨ΌνŠΈ {seg['segment_id'] + 1}** (μƒ˜ν”Œ {seg['start_idx']}-{seg['end_idx']})
76
  - 평균: {seg['mean']:.2f}
77
  - ν‘œμ€€νŽΈμ°¨: {seg['std']:.2f}
78
- - 관계식: y = {seg['slope']:.2f}x + {seg['intercept']:.2f}
 
 
 
 
 
79
  """
80
 
81
  return summary
 
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
  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
 
 
70
  def format_analysis_summary(analysis: Dict) -> str:
71
  """뢄석 κ²°κ³Όλ₯Ό μ‚¬λžŒμ΄ 읽기 μ‰¬μš΄ ν˜•μ‹μœΌλ‘œ 포맷"""
72
 
73
+ drift_type = analysis['drift_type']
74
+
75
  summary = f"""
76
  ## λ“œλ¦¬ν”„νŠΈ 뢄석 κ²°κ³Ό
77
 
78
+ **λ“œλ¦¬ν”„νŠΈ μœ ν˜•:** {drift_type.upper()}
79
 
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
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.linspace(0, 10, n_samples)
7
- y = np.zeros(n_samples)
8
 
9
- # Before drift: y = 2*X + noise
10
- y[:drift_point] = 2 * X[:drift_point] + np.random.normal(0, 1, drift_point)
11
 
12
- # After drift: y = -X + 5 + noise (μ™„μ „νžˆ λ‹€λ₯Έ 관계)
13
- y[drift_point:] = -X[drift_point:] + 5 + np.random.normal(0, 1, n_samples - drift_point)
14
 
15
  drift_points = np.array([drift_point])
16
  return X, y, drift_points
@@ -18,44 +18,41 @@ 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.linspace(0, 10, n_samples)
22
- y = np.zeros(n_samples)
23
 
24
- # Before drift: y = 2*X
25
- y[:drift_start] = 2 * X[:drift_start] + np.random.normal(0, 1, drift_start)
26
 
27
- # Gradual transition: mixture of old and new concepts
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 * X[i] + np.random.normal(0, 1)
32
- new_concept = -X[i] + 5 + np.random.normal(0, 1)
33
- y[i] = (1 - weight) * old_concept + weight * new_concept
34
 
35
- # After drift: y = -X + 5
36
- y[drift_end:] = -X[drift_end:] + 5 + np.random.normal(0, 1, 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.linspace(0, 10, 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
- slope = 2 - (step / n_steps) * 3 # 2μ—μ„œ -1둜 점진적 λ³€ν™”
56
- intercept = (step / n_steps) * 5 # 0μ—μ„œ 5둜 점진적 λ³€ν™”
57
-
58
- y[start_idx:end_idx] = slope * X[start_idx:end_idx] + intercept + np.random.normal(0, 1, end_idx - start_idx)
59
 
60
  if step > 0:
61
  drift_points.append(start_idx)
@@ -65,8 +62,8 @@ def generate_incremental_drift(n_samples: int = 1000, n_steps: int = 5) -> Tuple
65
 
66
  def generate_recurring_drift(n_samples: int = 1000, cycle_length: int = 250) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
67
  """반볡적 λ“œλ¦¬ν”„νŠΈ: 이전 뢄포가 주기적으둜 μž¬λ“±μž₯"""
68
- X = np.linspace(0, 10, n_samples)
69
- y = np.zeros(n_samples)
70
 
71
  drift_points = []
72
 
@@ -74,11 +71,11 @@ def generate_recurring_drift(n_samples: int = 1000, cycle_length: int = 250) ->
74
  cycle_pos = i % cycle_length
75
 
76
  if cycle_pos < cycle_length // 2:
77
- # Concept A: y = 2*X
78
- y[i] = 2 * X[i] + np.random.normal(0, 1)
79
  else:
80
- # Concept B: y = -X + 5
81
- y[i] = -X[i] + 5 + np.random.normal(0, 1)
82
 
83
  if cycle_pos == cycle_length // 2:
84
  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, 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
 
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
 
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
  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)
visualizer.py CHANGED
@@ -7,42 +7,60 @@ def create_drift_visualization(X: np.ndarray, y: np.ndarray, drift_points: np.nd
7
 
8
  fig = go.Figure()
9
 
10
- # 메인 데이터 scatter plot
11
- fig.add_trace(go.Scatter(
12
- x=X,
13
- y=y,
14
- mode='markers',
15
- name='Data Points',
16
- marker=dict(
17
- size=6,
18
- color=np.arange(len(X)),
19
- colorscale='Viridis',
20
- showscale=True,
21
- colorbar=dict(title="Time Step"),
22
- line=dict(width=0.5, color='white')
23
- ),
24
- hovertemplate='X: %{x:.2f}<br>y: %{y:.2f}<br>Index: %{marker.color}<extra></extra>'
25
- ))
26
-
27
- # λ“œλ¦¬ν”„νŠΈ λ°œμƒ 지점 ν‘œμ‹œ
28
- y_min, y_max = y.min(), y.max()
29
- y_range = y_max - y_min
30
-
31
- for i, drift_point in enumerate(drift_points):
32
- fig.add_vline(
33
- x=X[drift_point],
34
- line_dash="dash",
35
- line_color="red",
36
- annotation_text=f"Drift {i+1}",
37
- annotation_position="top"
38
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  # λ ˆμ΄μ•„μ›ƒ μ„€μ •
41
  title_map = {
42
- "sudden": "Sudden (Abrupt) Drift - κΈ‰κ²©ν•œ λ“œλ¦¬ν”„νŠΈ",
43
- "gradual": "Gradual Drift - 점진적 λ“œλ¦¬ν”„νŠΈ",
44
- "incremental": "Incremental Drift - 증뢄적 λ“œλ¦¬ν”„νŠΈ",
45
- "recurring": "Recurring Drift - 반볡적 λ“œλ¦¬ν”„νŠΈ"
46
  }
47
 
48
  fig.update_layout(
@@ -50,31 +68,31 @@ def create_drift_visualization(X: np.ndarray, y: np.ndarray, drift_points: np.nd
50
  text=title_map.get(drift_type, "Concept Drift"),
51
  x=0.5,
52
  xanchor='center',
53
- font=dict(size=20)
54
  ),
55
- xaxis_title="Feature (X)",
56
- yaxis_title="Target (y)",
57
  hovermode='closest',
58
  template='plotly_white',
59
- height=600,
60
- showlegend=True,
61
  legend=dict(
62
  yanchor="top",
63
  y=0.99,
64
- xanchor="left",
65
- x=0.01
66
  ),
67
  xaxis=dict(
68
- showgrid=True,
69
- gridwidth=1,
70
- gridcolor='LightGray'
71
  ),
72
  yaxis=dict(
73
- showgrid=True,
74
- gridwidth=1,
75
- gridcolor='LightGray'
76
  ),
77
- plot_bgcolor='white'
 
78
  )
79
 
80
  return fig
@@ -86,7 +104,9 @@ def create_comparison_visualization(drift_data_dict: dict) -> go.Figure:
86
 
87
  fig = make_subplots(
88
  rows=2, cols=2,
89
- subplot_titles=("Sudden Drift", "Gradual Drift", "Incremental Drift", "Recurring Drift")
 
 
90
  )
91
 
92
  positions = [(1, 1), (1, 2), (2, 1), (2, 2)]
@@ -96,28 +116,59 @@ def create_comparison_visualization(drift_data_dict: dict) -> go.Figure:
96
  if drift_type in drift_data_dict:
97
  X, y, drift_points = drift_data_dict[drift_type]
98
 
99
- fig.add_trace(
100
- go.Scatter(
101
- x=X,
102
- y=y,
103
- mode='markers',
104
- marker=dict(size=3, color=np.arange(len(X)), colorscale='Viridis'),
105
- showlegend=False
106
- ),
107
- row=row, col=col
108
- )
109
-
110
- # λ“œλ¦¬ν”„νŠΈ 지점 ν‘œμ‹œ
111
- for drift_point in drift_points:
112
- fig.add_vline(
113
- x=X[drift_point],
114
- line_dash="dash",
115
- line_color="red",
116
  row=row, col=col
117
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- fig.update_xaxes(title_text="X")
120
- fig.update_yaxes(title_text="y")
121
- fig.update_layout(height=800, title_text="Concept Drift Types Comparison", showlegend=False)
 
 
 
 
 
 
 
122
 
123
  return fig
 
7
 
8
  fig = go.Figure()
9
 
10
+ # incremental driftλŠ” 연속 κ°’μœΌλ‘œ 처리
11
+ if drift_type == "incremental":
12
+ # 0-1 사이 값을 μƒ‰μƒμœΌλ‘œ λ§€ν•‘
13
+ colors = []
14
+ for val in y:
15
+ # νŒŒλž€μƒ‰(0)μ—μ„œ μ΄ˆλ‘μƒ‰(1)둜 점진적 λ³€ν™”
16
+ blue = int(255 * (1 - val))
17
+ green = int(255 * val)
18
+ colors.append(f'rgb({blue}, {green}, 150)')
19
+
20
+ fig.add_trace(go.Bar(
21
+ x=X,
22
+ y=np.ones(len(X)),
23
+ marker=dict(
24
+ color=colors,
25
+ line=dict(width=0)
26
+ ),
27
+ showlegend=False,
28
+ hovertemplate='Time: %{x}<br>Value: %{customdata:.2f}<extra></extra>',
29
+ customdata=y
30
+ ))
31
+ else:
32
+ # 이진 λΆ„λ₯˜ (0: νŒŒλž€μƒ‰, 1: μ΄ˆλ‘μƒ‰)
33
+ class_0_indices = np.where(y == 0)[0]
34
+ class_1_indices = np.where(y == 1)[0]
35
+
36
+ # Class 0 (νŒŒλž€μƒ‰)
37
+ if len(class_0_indices) > 0:
38
+ fig.add_trace(go.Bar(
39
+ x=X[class_0_indices],
40
+ y=np.ones(len(class_0_indices)),
41
+ marker=dict(color='rgb(70, 130, 180)', line=dict(width=0)),
42
+ name='Class 0',
43
+ showlegend=True,
44
+ hovertemplate='Time: %{x}<br>Class: 0<extra></extra>'
45
+ ))
46
+
47
+ # Class 1 (μ΄ˆλ‘μƒ‰)
48
+ if len(class_1_indices) > 0:
49
+ fig.add_trace(go.Bar(
50
+ x=X[class_1_indices],
51
+ y=np.ones(len(class_1_indices)),
52
+ marker=dict(color='rgb(60, 179, 113)', line=dict(width=0)),
53
+ name='Class 1',
54
+ showlegend=True,
55
+ hovertemplate='Time: %{x}<br>Class: 1<extra></extra>'
56
+ ))
57
 
58
  # λ ˆμ΄μ•„μ›ƒ μ„€μ •
59
  title_map = {
60
+ "sudden": "Sudden Drift",
61
+ "gradual": "Gradual Drift",
62
+ "incremental": "Incremental Drift",
63
+ "recurring": "Reoccurring Concepts"
64
  }
65
 
66
  fig.update_layout(
 
68
  text=title_map.get(drift_type, "Concept Drift"),
69
  x=0.5,
70
  xanchor='center',
71
+ font=dict(size=20, weight='bold')
72
  ),
73
+ xaxis_title="Time",
74
+ yaxis_title="Data distribution",
75
  hovermode='closest',
76
  template='plotly_white',
77
+ height=400,
78
+ showlegend=(drift_type != "incremental"),
79
  legend=dict(
80
  yanchor="top",
81
  y=0.99,
82
+ xanchor="right",
83
+ x=0.99
84
  ),
85
  xaxis=dict(
86
+ showgrid=False,
87
+ showticklabels=False
 
88
  ),
89
  yaxis=dict(
90
+ showgrid=False,
91
+ showticklabels=False,
92
+ range=[0, 1.2]
93
  ),
94
+ plot_bgcolor='white',
95
+ bargap=0
96
  )
97
 
98
  return fig
 
104
 
105
  fig = make_subplots(
106
  rows=2, cols=2,
107
+ subplot_titles=("Sudden Drift", "Gradual Drift", "Incremental Drift", "Reoccurring Concepts"),
108
+ vertical_spacing=0.15,
109
+ horizontal_spacing=0.1
110
  )
111
 
112
  positions = [(1, 1), (1, 2), (2, 1), (2, 2)]
 
116
  if drift_type in drift_data_dict:
117
  X, y, drift_points = drift_data_dict[drift_type]
118
 
119
+ if drift_type == "incremental":
120
+ # Incremental drift: 연속 색상 λ³€ν™”
121
+ colors = []
122
+ for val in y:
123
+ blue = int(255 * (1 - val))
124
+ green = int(255 * val)
125
+ colors.append(f'rgb({blue}, {green}, 150)')
126
+
127
+ fig.add_trace(
128
+ go.Bar(
129
+ x=X,
130
+ y=np.ones(len(X)),
131
+ marker=dict(color=colors, line=dict(width=0)),
132
+ showlegend=False
133
+ ),
 
 
134
  row=row, col=col
135
  )
136
+ else:
137
+ # 이진 λΆ„λ₯˜
138
+ class_0_indices = np.where(y == 0)[0]
139
+ class_1_indices = np.where(y == 1)[0]
140
+
141
+ if len(class_0_indices) > 0:
142
+ fig.add_trace(
143
+ go.Bar(
144
+ x=X[class_0_indices],
145
+ y=np.ones(len(class_0_indices)),
146
+ marker=dict(color='rgb(70, 130, 180)', line=dict(width=0)),
147
+ showlegend=False
148
+ ),
149
+ row=row, col=col
150
+ )
151
+
152
+ if len(class_1_indices) > 0:
153
+ fig.add_trace(
154
+ go.Bar(
155
+ x=X[class_1_indices],
156
+ y=np.ones(len(class_1_indices)),
157
+ marker=dict(color='rgb(60, 179, 113)', line=dict(width=0)),
158
+ showlegend=False
159
+ ),
160
+ row=row, col=col
161
+ )
162
 
163
+ # λ ˆμ΄μ•„μ›ƒ μ„€μ •
164
+ fig.update_xaxes(title_text="Time", showgrid=False, showticklabels=False)
165
+ fig.update_yaxes(title_text="Data distribution", showgrid=False, showticklabels=False, range=[0, 1.2])
166
+ fig.update_layout(
167
+ height=800,
168
+ title_text="Concept Drift Types Comparison",
169
+ showlegend=False,
170
+ bargap=0,
171
+ template='plotly_white'
172
+ )
173
 
174
  return fig