Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions analysis/generate_html_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def generate_report():

for fpath in csv_files:
fname = os.path.basename(fpath)
if fname == 'detector_results.csv':
continue
label = KNOWN_LABELS.get(fname, fname)
if label == fname:
# Parse 20260217032620_326am_10h_23m.csv
Expand Down Expand Up @@ -251,24 +253,24 @@ def generate_report():
rows.forEach(row => {
let hrs = parseFloat(row.cells[3].dataset.sort) || 0;
totalHrs += hrs;

let evts = parseFloat(row.cells[12].innerText) || 0;
totalEvents += evts;

let pcArr = row.cells[13].innerText.split('/');
totalEvents10 += parseInt(pcArr[0] || 0);
totalEvents15 += parseInt(pcArr[1] || 0);

totalMajorA += parseInt(row.cells[17].innerText) || 0;
totalMajorB += parseInt(row.cells[18].innerText) || 0;
totalMajorC += parseInt(row.cells[19].innerText) || 0;

sumTab += (parseFloat(row.querySelector('.cell-tab').dataset.value) || 0) * hrs;
sumScore += (parseFloat(row.querySelector('.cell-score').dataset.value) || 0) * hrs;

sumDelta += (parseFloat(row.querySelector('.cell-delta').dataset.value) || 0) * evts;
sumP90 += (parseFloat(row.querySelector('.cell-p90').dataset.value) || 0) * evts;

let typeArr = row.cells[11].innerText.split('/');
sumTypeA += (parseFloat(typeArr[0]) || 0) * evts;
sumTypeB += (parseFloat(typeArr[1]) || 0) * evts;
Expand Down Expand Up @@ -316,7 +318,7 @@ def generate_report():
tr.innerHTML = `
<td><input type="checkbox" class="row-checkbox" checked></td>
<td class="left-align" style="white-space: nowrap;">${mergedDate}</td>
<td class="left-align"><span class="editable-label" contenteditable="true">Merged: ${labels.join(' + ')}</span></td>
<td class="left-align" style="font-size:11px;"><span class="editable-label" contenteditable="true">Merged: ${labels.join(' + ')}</span></td>
<td data-sort="${totalHrs}">${hrStr}</td>
<td class="cell-score" data-value="${newScore.toFixed(1)}">${newScore.toFixed(1)}</td>
<td class="cell-tab" data-value="${newTab.toFixed(1)}">${newTab.toFixed(1)}</td>
Expand Down Expand Up @@ -464,7 +466,7 @@ def generate_report():
<tr>
<th class="sorttable_nosort">Inc</th>
<th class="left-align" style="white-space: nowrap;">Date / Time</th>
<th class="left-align">Night Label</th>
<th class="left-align">Notes</th>
<th>Length</th>
<th>Score (0-100)</th>
<th>TAB</th>
Expand Down Expand Up @@ -516,7 +518,7 @@ def cell(metric_key, val):
dt = datetime.datetime.strptime(m_date.group(1), "%Y%m%d%H%M%S")
prev_dt = dt - datetime.timedelta(days=1)
time_str = dt.strftime("%I:%M%p").lstrip("0").lower()
date_str = f"{prev_dt.month}/{prev_dt.day}-{dt.month}/{dt.day} {time_str}"
date_str = f"{prev_dt.month}/{prev_dt.day}-{dt.month}/{dt.day}/{dt.strftime('%y')} {time_str}"
except:
pass

Expand All @@ -530,11 +532,28 @@ def cell(metric_key, val):

type_str = f"{r.get('pct_a', 0):.0f}/{r.get('pct_b', 0):.0f}/{r.get('pct_c', 0):.0f}"

# Uncheck daytime sessions by default (start hour before 8pm / after 6am)
is_daytime = False
m_hour = re.match(r'^\d{8}(\d{2})', fname)
if m_hour:
hour = int(m_hour.group(1))
if 6 <= hour < 20:
is_daytime = True
checked_attr = "" if is_daytime else "checked"

html.append(f"<tr data-filename='{fname}'>")
html.append(f'<td><input type="checkbox" class="row-checkbox" checked></td>')
html.append(f'<td><input type="checkbox" class="row-checkbox" {checked_attr}></td>')
chart_fname = fname.replace('.csv', '_chart.html')
html.append(f'<td class="left-align" style="white-space: nowrap;"><a href="javascript:openChart(\'charts/{chart_fname}\');" style="text-decoration:none; color:#0366d6;">{date_str}</a></td>')
html.append(f'<td class="left-align"><span class="editable-label" contenteditable="true">{r["label"]}</span></td>')
# Split label into display label and notes
label_text = r["label"]
# Strip date prefix and time/duration suffix, whatever remains is notes
tmp = label_text
tmp = re.sub(r'^\d{4}-\d{2}-\d{2}\s*', '', tmp)
tmp = re.sub(r'^\d+/\d+-\d+/\d+\s*', '', tmp)
tmp = re.sub(r'\d+[ap]m\s+\d+h\s+\d+m\s*$', '', tmp)
notes_text = tmp.strip()
html.append(f'<td class="left-align" style="font-size:11px; max-width:250px;"><span class="editable-label" contenteditable="true">{notes_text}</span></td>')
html.append(f'<td data-sort="{hrs_exact}">{hr_str}</td>')
html.append(cell('score', r['score']))
html.append(cell('tab', r['tab']))
Expand Down
18 changes: 16 additions & 2 deletions analysis/hr_spike_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ def load_data(filepath: str, source: str = 'auto') -> np.ndarray:

if hr_cols:
hr_col = hr_cols[0]

hr = df[hr_col].values.astype(float)
elif df.shape[1] == 1:
hr = df.iloc[:, 0].values.astype(float)
Expand All @@ -907,7 +907,21 @@ def load_data(filepath: str, source: str = 'auto') -> np.ndarray:
else:
print(f"Available columns: {list(df.columns)}")
raise ValueError("Could not identify HR column. Please specify.")


# Resample to 1Hz if timestamps indicate non-1s intervals
time_cols = [c for c in df.columns if c.lower() in ['time', 'timestamp']]
if time_cols and len(hr) >= 2:
try:
t = pd.to_datetime(df[time_cols[0]])
interval_s = (t.iloc[1] - t.iloc[0]).total_seconds()
if interval_s > 1.5: # Not already 1Hz
# Resample by repeating each value for its interval
hr_1hz = np.repeat(hr, int(round(interval_s)))
print(f" Resampled from {interval_s:.0f}s intervals: {len(hr)} -> {len(hr_1hz)} samples")
return hr_1hz
except Exception:
pass

return hr

except ImportError:
Expand Down
15 changes: 9 additions & 6 deletions analysis/run_detector_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,21 @@ def generate_session_chart(fpath, hr_smooth, baseline, valid, events, summary, c

os.makedirs(chart_dir, exist_ok=True)

# Read the timestamp column
# Read the timestamp column and generate 1Hz time axis matching resampled data
try:
df = pd.read_csv(fpath)
if 'Time' in df.columns:
t_axis = pd.to_datetime(df['Time'])
start_time_str = t_axis.iloc[0].strftime("%Y-%m-%d %H:%M:%S")
start_ts = t_axis.iloc[0].timestamp()
csv_times = pd.to_datetime(df['Time'])
start_time = csv_times.iloc[0]
start_time_str = start_time.strftime("%Y-%m-%d %H:%M:%S")
start_ts = start_time.timestamp()
# Generate 1Hz time axis to match resampled hr_smooth length
t_axis = pd.date_range(start=start_time, periods=len(hr_smooth), freq='1s')
else:
t_axis = np.arange(len(hr_smooth)) / 3600.0
start_time_str = ""
start_ts = 0

# Extract CSV content for in-browser export
csv_content = df.to_csv(index=False).replace('\\n', '\\\\n').replace('\\"', '\\\\"')
csv_js_string = f'`{csv_content}`'
Expand Down Expand Up @@ -386,7 +389,7 @@ def analyze_night(fpath, label, generate_chart=False, chart_dir=None):
out_lines.append(f"{'='*80}")

try:
# Load using the detector's own loader
# Load using the detector's own loader (resamples to 1Hz)
hr_raw = load_data(fpath, source='auto')
n = len(hr_raw)
hrs = n / 3600
Expand Down
Binary file added screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.