Data Understanding#

2.1 Sumber Data#

Dataset ECG200 secara historis merupakan bagian dari UCR/UEA Time Series Classification Archive dan saat ini tersedia melalui repositori resmi Time Series Classification.

Asal dan Cara Pengambilan Data#

  • Asal data: Dataset ini diformat oleh R. Olszewski sebagai bagian dari tesis doktoralnya di Carnegie Mellon University tahun 2001 (Generalized Feature Extraction for Structural Pattern Recognition in Time-Series Data).

  • Sumber sinyal: Setiap sampel mewakili satu denyut jantung, direkam dari aktivitas listrik jantung pasien menggunakan sistem perekaman ECG klinis.

  • Alat yang digunakan: Rekaman ECG diperoleh menggunakan perangkat elektrokardiografi medis standar, yang terdiri dari:

    • Elektroda permukaan yang ditempelkan pada dada dan anggota tubuh pasien,

    • Amplifier sinyal untuk memperkuat sinyal bioelektrik yang lemah,

    • Analog-to-Digital Converter (ADC) untuk mengubah sinyal analog menjadi data digital berupa deret waktu.

  • Proses pengambilan:

    • Sinyal ECG mentah direkam selama sesi klinis,

    • Segmen yang merepresentasikan satu siklus denyut jantung lengkap dipilih secara manual oleh ahli,

    • Segmen tersebut kemudian dipotong, dinormalisasi, dan diberi label berdasarkan diagnosis klinis pasien.

  • Jumlah pasien: Dataset tidak berasal dari hanya dua individu. Berdasarkan praktik umum dalam pembuatan dataset ECG medis dan sumber aslinya (termasuk MIT-BIH Arrhythmia Database), 200 sampel kemungkinan besar berasal dari puluhan pasien berbeda. Setiap pasien menyumbang 3–10 denyut jantung yang memenuhi kriteria kualitas dan representasi klinis.

  • Waktu pengambilan asli: Data dikumpulkan sebelum atau selama penelitian tesis (sekitar awal 2000-an).

  • Format data: Disimpan dalam format ARFF (Attribute-Relation File Format), sesuai standar WEKA.

  • Preprocessing oleh penyedia:

    • Setiap sampel mewakili satu denyut jantung, dengan panjang tetap 96 titik waktu.

    • Dinormalisasi secara global.

    • Label: 1 (normal heartbeat) dan -1 (Myocardial Infarction / Ischemia).

Komposisi Dataset#

  • Data latih: 100 sampel

  • Data uji: 100 sampel

  • Jumlah fitur per sampel: 96 (time steps)

  • Jumlah kelas: 2 (normal vs. infark miokard)

2.2 Eksplorasi Data#

2.2.1 Import Library dan Muat Data#

import pandas as pd
import numpy as np
from scipy.io import arff
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_style("whitegrid")
sns.set_palette("Set2")

train_path = 'ECG200/ECG200_TRAIN.arff'
test_path = 'ECG200/ECG200_TEST.arff'

data_train, _ = arff.loadarff(train_path)
data_test, _ = arff.loadarff(test_path)

df_train = pd.DataFrame(data_train)
df_test = pd.DataFrame(data_test)

df_train['target'] = df_train['target'].apply(lambda x: int(x.decode('utf-8')))
df_test['target'] = df_test['target'].apply(lambda x: int(x.decode('utf-8')))

print("✅ Data berhasil dimuat dan label telah dikonversi ke integer.")
✅ Data berhasil dimuat dan label telah dikonversi ke integer.

2.2.2 Struktur Kolom dan Informasi Dataset#

print("=== Struktur Kolom ===")
print(df_train.columns.tolist())

print("\n=== Informasi Dataset Latih ===")
df_train.info()

print("\n=== Statistik Deskriptif ===")
df_train.describe().T.head(10) 
=== Struktur Kolom ===
['att1', 'att2', 'att3', 'att4', 'att5', 'att6', 'att7', 'att8', 'att9', 'att10', 'att11', 'att12', 'att13', 'att14', 'att15', 'att16', 'att17', 'att18', 'att19', 'att20', 'att21', 'att22', 'att23', 'att24', 'att25', 'att26', 'att27', 'att28', 'att29', 'att30', 'att31', 'att32', 'att33', 'att34', 'att35', 'att36', 'att37', 'att38', 'att39', 'att40', 'att41', 'att42', 'att43', 'att44', 'att45', 'att46', 'att47', 'att48', 'att49', 'att50', 'att51', 'att52', 'att53', 'att54', 'att55', 'att56', 'att57', 'att58', 'att59', 'att60', 'att61', 'att62', 'att63', 'att64', 'att65', 'att66', 'att67', 'att68', 'att69', 'att70', 'att71', 'att72', 'att73', 'att74', 'att75', 'att76', 'att77', 'att78', 'att79', 'att80', 'att81', 'att82', 'att83', 'att84', 'att85', 'att86', 'att87', 'att88', 'att89', 'att90', 'att91', 'att92', 'att93', 'att94', 'att95', 'att96', 'target']

=== Informasi Dataset Latih ===
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100 entries, 0 to 99
Data columns (total 97 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   att1    100 non-null    float64
 1   att2    100 non-null    float64
 2   att3    100 non-null    float64
 3   att4    100 non-null    float64
 4   att5    100 non-null    float64
 5   att6    100 non-null    float64
 6   att7    100 non-null    float64
 7   att8    100 non-null    float64
 8   att9    100 non-null    float64
 9   att10   100 non-null    float64
 10  att11   100 non-null    float64
 11  att12   100 non-null    float64
 12  att13   100 non-null    float64
 13  att14   100 non-null    float64
 14  att15   100 non-null    float64
 15  att16   100 non-null    float64
 16  att17   100 non-null    float64
 17  att18   100 non-null    float64
 18  att19   100 non-null    float64
 19  att20   100 non-null    float64
 20  att21   100 non-null    float64
 21  att22   100 non-null    float64
 22  att23   100 non-null    float64
 23  att24   100 non-null    float64
 24  att25   100 non-null    float64
 25  att26   100 non-null    float64
 26  att27   100 non-null    float64
 27  att28   100 non-null    float64
 28  att29   100 non-null    float64
 29  att30   100 non-null    float64
 30  att31   100 non-null    float64
 31  att32   100 non-null    float64
 32  att33   100 non-null    float64
 33  att34   100 non-null    float64
 34  att35   100 non-null    float64
 35  att36   100 non-null    float64
 36  att37   100 non-null    float64
 37  att38   100 non-null    float64
 38  att39   100 non-null    float64
 39  att40   100 non-null    float64
 40  att41   100 non-null    float64
 41  att42   100 non-null    float64
 42  att43   100 non-null    float64
 43  att44   100 non-null    float64
 44  att45   100 non-null    float64
 45  att46   100 non-null    float64
 46  att47   100 non-null    float64
 47  att48   100 non-null    float64
 48  att49   100 non-null    float64
 49  att50   100 non-null    float64
 50  att51   100 non-null    float64
 51  att52   100 non-null    float64
 52  att53   100 non-null    float64
 53  att54   100 non-null    float64
 54  att55   100 non-null    float64
 55  att56   100 non-null    float64
 56  att57   100 non-null    float64
 57  att58   100 non-null    float64
 58  att59   100 non-null    float64
 59  att60   100 non-null    float64
 60  att61   100 non-null    float64
 61  att62   100 non-null    float64
 62  att63   100 non-null    float64
 63  att64   100 non-null    float64
 64  att65   100 non-null    float64
 65  att66   100 non-null    float64
 66  att67   100 non-null    float64
 67  att68   100 non-null    float64
 68  att69   100 non-null    float64
 69  att70   100 non-null    float64
 70  att71   100 non-null    float64
 71  att72   100 non-null    float64
 72  att73   100 non-null    float64
 73  att74   100 non-null    float64
 74  att75   100 non-null    float64
 75  att76   100 non-null    float64
 76  att77   100 non-null    float64
 77  att78   100 non-null    float64
 78  att79   100 non-null    float64
 79  att80   100 non-null    float64
 80  att81   100 non-null    float64
 81  att82   100 non-null    float64
 82  att83   100 non-null    float64
 83  att84   100 non-null    float64
 84  att85   100 non-null    float64
 85  att86   100 non-null    float64
 86  att87   100 non-null    float64
 87  att88   100 non-null    float64
 88  att89   100 non-null    float64
 89  att90   100 non-null    float64
 90  att91   100 non-null    float64
 91  att92   100 non-null    float64
 92  att93   100 non-null    float64
 93  att94   100 non-null    float64
 94  att95   100 non-null    float64
 95  att96   100 non-null    float64
 96  target  100 non-null    int64  
dtypes: float64(96), int64(1)
memory usage: 75.9 KB

=== Statistik Deskriptif ===
count mean std min 25% 50% 75% max
att1 100.0 0.708438 0.593513 -0.706305 0.318643 0.581008 0.994760 2.689017
att2 100.0 1.422151 0.927972 -1.100715 0.779810 1.232191 2.093703 3.535038
att3 100.0 2.039149 1.046880 -1.321589 1.525548 2.152552 2.707411 3.850263
att4 100.0 2.126455 1.098754 -1.076313 1.511639 2.234408 2.865394 4.199145
att5 100.0 1.551616 0.954734 -1.247922 0.966292 1.746861 2.163355 3.720899
att6 100.0 0.762251 0.963111 -1.482391 0.022470 0.574297 1.502335 3.026452
att7 100.0 0.282647 1.096712 -1.598712 -0.603378 0.120230 1.226563 2.454195
att8 100.0 0.333630 0.877640 -1.508060 -0.464767 0.366122 1.008876 2.220327
att9 100.0 0.372973 0.761753 -1.609777 -0.075355 0.457255 0.990675 2.122435
att10 100.0 0.210769 0.739185 -1.600454 -0.205867 0.182365 0.776566 1.916524

2.2.3 Identifikasi Missing Value#

print("=== Jumlah Missing Value ===")
missing_train = df_train.isnull().sum()
missing_test = df_test.isnull().sum()

print("Data Latih:")
if missing_train.sum() == 0:
    print("Tidak ada missing value.")
else:
    print(missing_train[missing_train > 0])

print("\nData Uji:")
if missing_test.sum() == 0:
    print("Tidak ada missing value.")
else:
    print(missing_test[missing_test > 0])

fig, ax = plt.subplots(1, 2, figsize=(12, 4))

# Heatmap hanya menunjukkan keberadaan missing (True = putih/merah, False = putih)
sns.heatmap(df_train.isnull(), cbar=False, cmap='Reds', ax=ax[0], yticklabels=False, xticklabels=False)
ax[0].set_title('Missing Value - Data Latih', fontsize=12)
ax[0].set_facecolor('white')

sns.heatmap(df_test.isnull(), cbar=False, cmap='Reds', ax=ax[1], yticklabels=False, xticklabels=False)
ax[1].set_title('Missing Value - Data Uji', fontsize=12)
ax[1].set_facecolor('white')

fig.patch.set_facecolor('white')
for a in ax:
    a.figure.set_facecolor('white')

plt.tight_layout()
plt.show()
=== Jumlah Missing Value ===
Data Latih:
Tidak ada missing value.

Data Uji:
Tidak ada missing value.
_images/e19e7c3802b392e151611f0e9b85e71c1e679c06e0be60291170d3536ebb6840.png

2.2.4 Distribusi Kelas#

print("=== Distribusi Kelas ===")
print("Data Latih:")
print(df_train['target'].value_counts().sort_index())

print("\nData Uji:")
print(df_test['target'].value_counts().sort_index())


fig, ax = plt.subplots(1, 2, figsize=(10, 4))

sns.countplot(data=df_train, x='target', ax=ax[0], palette='pastel')
ax[0].set_title('Distribusi Kelas - Data Latih')

sns.countplot(data=df_test, x='target', ax=ax[1], palette='pastel')
ax[1].set_title('Distribusi Kelas - Data Uji')

plt.tight_layout()
plt.show()
=== Distribusi Kelas ===
Data Latih:
target
-1    31
 1    69
Name: count, dtype: int64

Data Uji:
target
-1    36
 1    64
Name: count, dtype: int64
/tmp/ipykernel_17518/1495402320.py:12: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.countplot(data=df_train, x='target', ax=ax[0], palette='pastel')
/tmp/ipykernel_17518/1495402320.py:15: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.countplot(data=df_test, x='target', ax=ax[1], palette='pastel')
_images/daff440e1267afe6d9f3d08f62f47d47fdd28e22f86a2cc9b6b25390cbbdbcd0.png

2.2.5 Visualisasi Time Series Contoh Per Kelas#

print("Jumlah sampel normal:", len(df_train[df_train['target'] == 1]))
print("Jumlah sampel abnormal:", len(df_train[df_train['target'] == -1]))

if len(df_train[df_train['target'] == 1]) == 0 or len(df_train[df_train['target'] == -1]) == 0:
    print("⚠️ Peringatan: Tidak ada data ditemukan untuk salah satu kelas!")
else:
    fig, axes = plt.subplots(2, 2, figsize=(12, 8))
    axes = axes.flatten()

    sample_normal = df_train[df_train['target'] == 1].iloc[:2]
    sample_abnormal = df_train[df_train['target'] == -1].iloc[:2]

    for i, row in enumerate(sample_normal.iterrows()):
        idx, data = row
        axes[i].plot(data[:-1], color='green', linewidth=2)
        axes[i].set_title(f'Normal Sample {i+1}')
        axes[i].grid(True, alpha=0.3)

    for i, row in enumerate(sample_abnormal.iterrows()):
        idx, data = row
        axes[i+2].plot(data[:-1], color='red', linewidth=2)
        axes[i+2].set_title(f'Abnormal Sample {i+1}')
        axes[i+2].grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()
Jumlah sampel normal: 69
Jumlah sampel abnormal: 31
_images/3d0279ac1aeebb5ef0fc172196fc7968211491dc6215bba679249523c3ba380b.png

2.2.6 Deteksi Outlier Menggunakan ABOD (Angle-Based Outlier Detection)#

from pyod.models.abod import ABOD

df_combined = pd.concat([df_train, df_test], ignore_index=True)

X = df_combined.iloc[:, :-1].values 
y = df_combined['target'].values

abod = ABOD(contamination=0.05)  
outlier_labels = abod.fit_predict(X)

df_combined['outlier'] = outlier_labels

n_outliers = (outlier_labels == -1).sum()
print(f"🔍 Jumlah outlier terdeteksi: {n_outliers} dari {len(df_combined)} sampel")

fig, ax = plt.subplots(figsize=(10, 6))

normal_sample = df_combined[df_combined['outlier'] == 1].iloc[0, :-2].values
ax.plot(normal_sample, color='green', label='Normal Sample', linewidth=2)

if n_outliers > 0:
    outlier_sample = df_combined[df_combined['outlier'] == -1].iloc[0, :-2].values
    ax.plot(outlier_sample, color='red', label='Detected Outlier', linewidth=2, linestyle='--')
else:
    print("⚠️ Tidak ada outlier terdeteksi — hanya menampilkan sampel normal.")

ax.set_title('Contoh Time Series: Normal vs. Outlier (ABOD)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.show()
/usr/local/python/3.12.1/lib/python3.12/site-packages/sklearn/utils/deprecation.py:87: FutureWarning: Function fit_predict is deprecated
  warnings.warn(msg, category=FutureWarning)
🔍 Jumlah outlier terdeteksi: 0 dari 200 sampel
⚠️ Tidak ada outlier terdeteksi — hanya menampilkan sampel normal.
_images/4da3b414feabf2d9535c7f8090372f9b5641392252e826e26a5b7ff1d2b90fb7.png