Skip to content

Commit 9ef7383

Browse files
authored
Merge pull request patr1ck-m#29 from patr1ck-m/feature/plot_data
Add python plotting script for raw data and anomalies
2 parents 98b25e3 + 679591d commit 9ef7383

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

plot_data.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""
2+
Plot anomalies and raw data from CSV files.
3+
"""
4+
5+
import pandas as pd
6+
import matplotlib.pyplot as plt
7+
from datetime import datetime
8+
import argparse
9+
10+
11+
def convert_timestamp(timestamp_ms):
12+
"""Converts Unix timestamp in milliseconds to a readable datetime format"""
13+
timestamp_sec = timestamp_ms / 1000
14+
dt = datetime.fromtimestamp(timestamp_sec)
15+
return dt
16+
17+
18+
def plot_raw_data(csv_file, value_name='Value', out_file=None):
19+
"""Plots raw data values"""
20+
21+
print(f"Loading raw data from {csv_file}...")
22+
df = pd.read_csv(csv_file)
23+
24+
# Convert timestamps
25+
df['datetime'] = df['timestamp'].apply(convert_timestamp)
26+
27+
# Create plot
28+
plt.figure(figsize=(14, 6))
29+
plt.plot(df['datetime'], df['value'], marker='o', linestyle='-',
30+
markersize=3, linewidth=1, color='steelblue', label=value_name)
31+
32+
# Formatting
33+
plt.title(f'{value_name} - Raw Data Plot', fontsize=14, fontweight='bold')
34+
plt.xlabel('Time', fontsize=12)
35+
plt.ylabel(value_name, fontsize=12)
36+
plt.grid(True, alpha=0.3)
37+
plt.legend()
38+
39+
# Rotate x-axis labels by 45 degrees for better readability
40+
plt.xticks(rotation=45, ha='right')
41+
plt.tight_layout()
42+
43+
if out_file:
44+
plt.savefig(out_file)
45+
print(f"Plot saved to {out_file}")
46+
else:
47+
plt.show()
48+
49+
50+
def plot_anomalies(csv_file, value_name='Value', show_anomaly_values=False, out_file=None):
51+
"""Plots data with marked anomalies and background shading"""
52+
53+
print(f"Loading anomaly data from {csv_file}...")
54+
df = pd.read_csv(csv_file)
55+
56+
# Convert timestamps
57+
df['datetime'] = df['timestamp'].apply(convert_timestamp)
58+
59+
# Convert boolean values (if needed)
60+
df['isAnomaly'] = df['isAnomaly'].astype(bool)
61+
62+
# Create plot
63+
fig, ax = plt.subplots(figsize=(14, 6))
64+
65+
# Find continuous anomaly regions
66+
anomaly_regions = []
67+
in_anomaly = False
68+
start_idx = None
69+
70+
for idx, row in enumerate(df.itertuples(index=False)):
71+
if row.isAnomaly and not in_anomaly:
72+
# Start of an anomaly region
73+
in_anomaly = True
74+
start_idx = idx
75+
elif not row.isAnomaly and in_anomaly:
76+
# End of an anomaly region
77+
in_anomaly = False
78+
anomaly_regions.append((start_idx, idx - 1))
79+
80+
# Handle case where data ends with an anomaly
81+
if in_anomaly:
82+
anomaly_regions.append((start_idx, len(df) - 1))
83+
84+
# Add red background for anomaly regions
85+
for start_idx, end_idx in anomaly_regions:
86+
start_time = df.iloc[start_idx]['datetime']
87+
end_time = df.iloc[end_idx]['datetime']
88+
ax.axvspan(start_time, end_time, alpha=0.2, color='red', zorder=0)
89+
90+
# Plot all values as continuous line
91+
ax.plot(df['datetime'], df['value'], marker='o', linestyle='-',
92+
markersize=3, linewidth=1, color='steelblue', label=value_name, zorder=2)
93+
94+
# Color anomaly points differently
95+
anomalies = df[df['isAnomaly']]
96+
ax.scatter(anomalies['datetime'], anomalies['value'],
97+
color='red', s=80, marker='o', label='Anomaly', zorder=3)
98+
99+
# Optionally plot anomaly scores
100+
if show_anomaly_values:
101+
# Create secondary y-axis for anomaly scores
102+
ax2 = ax.twinx()
103+
104+
# Plot anomaly score as line with markers
105+
ax2.plot(df['datetime'], df['anomalyScore'], marker='s', linestyle='--',
106+
markersize=3, linewidth=1, color='orange', label='Anomaly Score', zorder=2, alpha=0.7)
107+
108+
# Format secondary axis
109+
ax2.set_ylabel('Anomaly Score', fontsize=12, color='orange')
110+
ax2.tick_params(axis='y', labelcolor='orange')
111+
112+
# Combine legends from both axes
113+
lines1, labels1 = ax.get_legend_handles_labels()
114+
lines2, labels2 = ax2.get_legend_handles_labels()
115+
ax.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
116+
117+
# Formatting
118+
ax.set_title(f'{value_name} with Anomaly Detection', fontsize=14, fontweight='bold')
119+
ax.set_xlabel('Time', fontsize=12)
120+
ax.set_ylabel(value_name, fontsize=12)
121+
ax.grid(True, alpha=0.3)
122+
123+
if not show_anomaly_values:
124+
ax.legend()
125+
126+
# Rotate x-axis by 45 degrees
127+
plt.xticks(rotation=45, ha='right')
128+
plt.tight_layout()
129+
130+
if out_file:
131+
plt.savefig(out_file)
132+
print(f"Plot saved to {out_file}")
133+
else:
134+
plt.show()
135+
136+
137+
def main():
138+
"""Main function with argument parser"""
139+
140+
parser = argparse.ArgumentParser(
141+
description='Plots data from CSV files',
142+
formatter_class=argparse.RawDescriptionHelpFormatter,
143+
epilog="""
144+
Examples:
145+
python plot_data.py --raw cpu_usage_raw.csv --value-name "CPU Usage"
146+
python plot_data.py --anomalies cpu_anomalies.csv --value-name "CPU Usage"
147+
python plot_data.py --anomalies cpu_anomalies.csv --value-name "CPU Usage" --show-anomaly-values
148+
python plot_data.py --raw data.csv --value-name "Temperature"
149+
"""
150+
)
151+
152+
# Define arguments
153+
parser.add_argument('--raw', type=str, metavar='FILE',
154+
help='Plots raw data')
155+
parser.add_argument('--anomalies', type=str, metavar='FILE',
156+
help='Plots data with anomaly detection')
157+
parser.add_argument('--value-name', type=str, metavar='NAME', default='Value',
158+
help='Name of the values being plotted (e.g., CPU Usage, Memory, Temperature)')
159+
parser.add_argument('--show-anomaly-values', action='store_true',
160+
help='Show anomaly scores on a secondary y-axis')
161+
parser.add_argument('--out', type=str, metavar='FILE',
162+
help='Output file to save the plot')
163+
164+
args = parser.parse_args()
165+
166+
# At least one option must be chosen
167+
if not args.raw and not args.anomalies:
168+
print("Error: At least one of --raw or --anomalies must be specified")
169+
print()
170+
parser.print_help()
171+
return
172+
173+
# Plot data
174+
if args.raw:
175+
plot_raw_data(args.raw, args.value_name, args.out)
176+
177+
if args.anomalies:
178+
plot_anomalies(args.anomalies, args.value_name, args.show_anomaly_values, args.out)
179+
180+
181+
if __name__ == "__main__":
182+
main()

0 commit comments

Comments
 (0)