import csv import re from datetime import datetime, timedelta from collections import defaultdict # Load CGM data cgm = [] with open('/home/ben/downloads/Blood Glucose-2026-02-19-2026-03-17.csv') as f: reader = csv.DictReader(f) for row in reader: dt = datetime.strptime(row['Date/Time'], '%Y-%m-%d %H:%M:%S') bg = float(row['Blood Glucose (mg/dL)']) cgm.append((dt, bg)) cgm.sort(key=lambda x: x[0]) # Parse meal log meals = [] with open('/home/ben/ava/ben/health/meal-log.md') as f: content = f.read() current_date = None for line in content.strip().split('\n'): line = line.strip() if line.startswith('# '): date_str = line[2:].strip() # Parse various date formats for fmt in ['%B %d', '%b %d']: try: d = datetime.strptime(date_str, fmt) current_date = d.replace(year=2026) break except: continue elif line and current_date and re.match(r'\d{1,2}:\d{2}', line): parts = line.split(' ', 1) time_str = parts[0] food = parts[1] if len(parts) > 1 else '' try: t = datetime.strptime(time_str, '%H:%M') meal_dt = current_date.replace(hour=t.hour, minute=t.minute) meals.append((meal_dt, food.strip())) except: pass meals.sort(key=lambda x: x[0]) print(f"Loaded {len(cgm)} CGM readings, {len(meals)} meals") print(f"CGM range: {cgm[0][0]} to {cgm[-1][0]}") print(f"Meal range: {meals[0][0]} to {meals[-1][0]}") # Helper: get CGM readings in a window def get_cgm_window(start, end): return [(dt, bg) for dt, bg in cgm if start <= dt <= end] def get_bg_at(target, window_min=30): """Get closest BG reading within window_min minutes of target""" closest = None closest_diff = timedelta(minutes=window_min) for dt, bg in cgm: diff = abs(dt - target) if diff < closest_diff: closest = bg closest_diff = diff return closest def get_peak_after(meal_time, hours=2.5): """Get peak BG in hours after meal""" window = get_cgm_window(meal_time, meal_time + timedelta(hours=hours)) if not window: return None, None peak = max(window, key=lambda x: x[1]) return peak[1], peak[0] def get_pre_meal_bg(meal_time, window_min=30): """Get BG just before meal""" window = get_cgm_window(meal_time - timedelta(minutes=window_min), meal_time + timedelta(minutes=5)) if not window: return None # Get the reading closest to meal time closest = min(window, key=lambda x: abs(x[0] - meal_time)) return closest[1] # Group meals by day days = defaultdict(list) for dt, food in meals: days[dt.date()].append((dt, food)) # ============================================================ # Q1: FASTING DURATION THRESHOLD ANALYSIS # ============================================================ print("\n" + "="*60) print("Q1: FASTING DURATION THRESHOLDS") print("="*60) # Calculate fasting duration for each meal (time since last meal on previous day or same day) fasting_data = [] all_meal_times = sorted([(dt, food) for dt, food in meals]) for i, (dt, food) in enumerate(all_meal_times): # Find previous meal if i > 0: prev_dt = all_meal_times[i-1][0] fast_hours = (dt - prev_dt).total_seconds() / 3600 # Only count as "fasting" if it's the first meal of the day (fast > 8h) if fast_hours >= 8: pre_bg = get_pre_meal_bg(dt) peak_bg, peak_time = get_peak_after(dt) if pre_bg and peak_bg: spike = peak_bg - pre_bg fasting_data.append({ 'date': dt.date(), 'fast_hours': fast_hours, 'pre_bg': pre_bg, 'peak_bg': peak_bg, 'spike': spike, 'food': food, 'meal_time': dt }) print(f"\n{len(fasting_data)} first-meals-of-day with CGM overlap\n") # Sort by fasting duration fasting_data.sort(key=lambda x: x['fast_hours']) # Buckets buckets = { '10-14h': [d for d in fasting_data if 10 <= d['fast_hours'] < 14], '14-16h': [d for d in fasting_data if 14 <= d['fast_hours'] < 16], '16-18h': [d for d in fasting_data if 16 <= d['fast_hours'] < 18], '18-20h': [d for d in fasting_data if 18 <= d['fast_hours'] < 20], '20-24h': [d for d in fasting_data if 20 <= d['fast_hours'] < 24], '24h+': [d for d in fasting_data if d['fast_hours'] >= 24], } print(f"{'Bucket':<10} {'n':<4} {'Avg Pre-BG':<12} {'Avg Spike':<12} {'Avg Peak':<12}") print("-" * 55) for bucket, data in buckets.items(): if data: avg_pre = sum(d['pre_bg'] for d in data) / len(data) avg_spike = sum(d['spike'] for d in data) / len(data) avg_peak = sum(d['peak_bg'] for d in data) / len(data) print(f"{bucket:<10} {len(data):<4} {avg_pre:<12.1f} {avg_spike:<12.1f} {avg_peak:<12.1f}") print("\nDetailed breakdown:") for d in fasting_data: print(f" {d['date']} | {d['fast_hours']:.1f}h fast | pre:{d['pre_bg']:.0f} | peak:{d['peak_bg']:.0f} | spike:+{d['spike']:.0f} | {d['food'][:50]}") # Correlation if len(fasting_data) >= 3: import statistics fasts = [d['fast_hours'] for d in fasting_data] pre_bgs = [d['pre_bg'] for d in fasting_data] spikes = [d['spike'] for d in fasting_data] # Pearson correlation (manual) def pearson(x, y): n = len(x) mx, my = sum(x)/n, sum(y)/n sx = (sum((xi-mx)**2 for xi in x) / (n-1)) ** 0.5 sy = (sum((yi-my)**2 for yi in y) / (n-1)) ** 0.5 cov = sum((xi-mx)*(yi-my) for xi, yi in zip(x, y)) / (n-1) return cov / (sx * sy) if sx > 0 and sy > 0 else 0 r_pre = pearson(fasts, pre_bgs) r_spike = pearson(fasts, spikes) print(f"\nCorrelation (fast duration vs pre-meal BG): r = {r_pre:.3f}") print(f"Correlation (fast duration vs spike size): r = {r_spike:.3f}") # ============================================================ # Q2: BEER/SODA EFFECT # ============================================================ print("\n" + "="*60) print("Q2: BEER/SODA WITH MEALS") print("="*60) # Tag meals with alcohol/soda beverage_meals = [] non_beverage_meals = [] # Look at each day's eating window for date, day_meals in sorted(days.items()): day_foods = ' '.join(f.lower() for _, f in day_meals) # Check each meal for beverages for dt, food in day_meals: food_lower = food.lower() has_beer = any(w in food_lower for w in ['beer', 'porter', 'bud light', 'ale', 'ipa']) has_soda = any(w in food_lower for w in ['soda', 'coke', 'olipop', 'poppi', 'cream soda']) has_sugary_drink = any(w in food_lower for w in ['orange juice', 'mango lassi', 'milk and honey']) pre_bg = get_pre_meal_bg(dt) peak_bg, _ = get_peak_after(dt) if pre_bg and peak_bg: entry = { 'date': date, 'time': dt, 'food': food, 'pre_bg': pre_bg, 'peak_bg': peak_bg, 'spike': peak_bg - pre_bg, 'has_beer': has_beer, 'has_soda': has_soda, 'has_sugary_drink': has_sugary_drink } if has_beer or has_soda or has_sugary_drink: beverage_meals.append(entry) else: non_beverage_meals.append(entry) print(f"\nMeals WITH beer/soda/sugary drinks: {len(beverage_meals)}") for m in beverage_meals: bev_type = [] if m['has_beer']: bev_type.append('BEER') if m['has_soda']: bev_type.append('SODA') if m['has_sugary_drink']: bev_type.append('SUGARY') print(f" {m['date']} {m['time'].strftime('%H:%M')} [{'/'.join(bev_type)}] pre:{m['pre_bg']:.0f} peak:{m['peak_bg']:.0f} spike:+{m['spike']:.0f} | {m['food'][:60]}") print(f"\nMeals WITHOUT drinks (for comparison): {len(non_beverage_meals)}") if beverage_meals: avg_bev_spike = sum(m['spike'] for m in beverage_meals) / len(beverage_meals) print(f" Avg spike with drinks: +{avg_bev_spike:.1f}") if non_beverage_meals: avg_no_spike = sum(m['spike'] for m in non_beverage_meals) / len(non_beverage_meals) print(f" Avg spike without drinks: +{avg_no_spike:.1f}") # Separate beer from soda beer_meals = [m for m in beverage_meals if m['has_beer']] soda_meals = [m for m in beverage_meals if m['has_soda'] and not m['has_beer']] if beer_meals: avg_beer = sum(m['spike'] for m in beer_meals) / len(beer_meals) print(f" Beer specifically (n={len(beer_meals)}): avg spike +{avg_beer:.1f}") for m in beer_meals: print(f" {m['date']} pre:{m['pre_bg']:.0f} peak:{m['peak_bg']:.0f} +{m['spike']:.0f} | {m['food'][:55]}") if soda_meals: avg_soda = sum(m['spike'] for m in soda_meals) / len(soda_meals) print(f" Soda only (n={len(soda_meals)}): avg spike +{avg_soda:.1f}") for m in soda_meals: print(f" {m['date']} pre:{m['pre_bg']:.0f} peak:{m['peak_bg']:.0f} +{m['spike']:.0f} | {m['food'][:55]}") # ============================================================ # Q3: RICE - HOW MUCH DATA? # ============================================================ print("\n" + "="*60) print("Q3: RICE DATA SUFFICIENCY") print("="*60) rice_meals = [] starch_meals = [] # comparison starches for dt, food in meals: food_lower = food.lower() pre_bg = get_pre_meal_bg(dt) peak_bg, _ = get_peak_after(dt) if pre_bg and peak_bg: entry = {'date': dt.date(), 'time': dt, 'food': food, 'pre_bg': pre_bg, 'peak_bg': peak_bg, 'spike': peak_bg - pre_bg} if 'rice' in food_lower and 'rice cake' not in food_lower: rice_meals.append(entry) elif any(w in food_lower for w in ['pasta', 'spaghetti', 'noodle', 'macaroni']): starch_meals.append(entry) print(f"\nRice meals with CGM data: {len(rice_meals)}") for m in rice_meals: print(f" {m['date']} pre:{m['pre_bg']:.0f} peak:{m['peak_bg']:.0f} spike:+{m['spike']:.0f} | {m['food']}") print(f"\nPasta/noodle meals (comparison starch): {len(starch_meals)}") for m in starch_meals: print(f" {m['date']} pre:{m['pre_bg']:.0f} peak:{m['peak_bg']:.0f} spike:+{m['spike']:.0f} | {m['food']}") # Also check: rice cake, quinoa other_grains = [] for dt, food in meals: food_lower = food.lower() pre_bg = get_pre_meal_bg(dt) peak_bg, _ = get_peak_after(dt) if pre_bg and peak_bg: if any(w in food_lower for w in ['quinoa', 'rice cake', 'mamaliga', 'oat', 'corn']): other_grains.append({'date': dt.date(), 'food': food, 'pre_bg': pre_bg, 'peak_bg': peak_bg, 'spike': peak_bg - pre_bg}) print(f"\nOther grains/starches for reference:") for m in other_grains: print(f" {m['date']} pre:{m['pre_bg']:.0f} peak:{m['peak_bg']:.0f} spike:+{m['spike']:.0f} | {m['food']}") # ============================================================ # Q4: DATA HOLES / EXPERIMENT SUGGESTIONS # ============================================================ print("\n" + "="*60) print("Q4: DATA GAPS & EXPERIMENT IDEAS") print("="*60) # Check for days with CGM but no meals cgm_dates = set() for dt, bg in cgm: cgm_dates.add(dt.date()) meal_dates = set(d for d in days.keys()) cgm_only = sorted(cgm_dates - meal_dates) meal_only = sorted(meal_dates - cgm_dates) print(f"\nDays with CGM but NO meal log: {len(cgm_only)}") for d in cgm_only: print(f" {d}") print(f"\nDays with meal log but NO CGM: {len(meal_only)}") for d in meal_only: print(f" {d}") # Food categories and sample sizes categories = defaultdict(list) for dt, food in meals: food_lower = food.lower() pre_bg = get_pre_meal_bg(dt) peak_bg, _ = get_peak_after(dt) if not (pre_bg and peak_bg): continue spike = peak_bg - pre_bg if 'rice' in food_lower and 'rice cake' not in food_lower: categories['rice'].append(spike) elif any(w in food_lower for w in ['pasta', 'spaghetti', 'noodle', 'macaroni']): categories['pasta/noodles'].append(spike) elif any(w in food_lower for w in ['bread', 'toast', 'roll', 'pita', 'croissant', 'cornbread']): categories['bread/wheat'].append(spike) elif any(w in food_lower for w in ['egg', 'eggs']): categories['eggs'].append(spike) elif any(w in food_lower for w in ['yogurt', 'yogurth']): categories['yogurt'].append(spike) elif any(w in food_lower for w in ['steak', 'beef', 'chicken', 'pork', 'shrimp', 'fish', 'salmon', 'burger', 'gyro', 'tilapia', 'sardine']): categories['protein-heavy'].append(spike) elif any(w in food_lower for w in ['fig', 'apple', 'banana', 'fruit', 'mango', 'peach', 'strawberry', 'blueberr', 'cherry', 'watermelon']): categories['fruit'].append(spike) elif any(w in food_lower for w in ['cake', 'cookie', 'ice cream', 'chocolate', 'pudding', 'cobbler', 'pancake', 'syrup']): categories['sweets/dessert'].append(spike) print(f"\nCategory sample sizes:") for cat, spikes in sorted(categories.items(), key=lambda x: -sum(x[1])/len(x[1]) if x[1] else 0): avg = sum(spikes)/len(spikes) print(f" {cat:<20} n={len(spikes):<3} avg spike: +{avg:.1f} (range: +{min(spikes):.0f} to +{max(spikes):.0f})") # ============================================================ # Q5: TIME TO GLUCONEOGENESIS (TTGNG) # ============================================================ print("\n" + "="*60) print("Q5: TIME TO GLUCONEOGENESIS (TTGNG)") print("="*60) print(""" TTGNG = time from last meal until liver switches from glycogen → gluconeogenesis. CGM signature: glucose drops to a nadir, then either flattens or slightly rises (counter-regulatory hormones kick in as glycogen depletes). Looking for the inflection point in overnight/fasting glucose traces... """) # For each day, find the last meal, then trace glucose through the night for date, day_meals in sorted(days.items()): if not day_meals: continue last_meal_dt = max(dt for dt, _ in day_meals) last_meal_food = [f for dt, f in day_meals if dt == last_meal_dt][0] # Get glucose from last meal through next 18 hours window = get_cgm_window(last_meal_dt, last_meal_dt + timedelta(hours=18)) if len(window) < 10: continue # Find post-meal peak post_peak_window = get_cgm_window(last_meal_dt, last_meal_dt + timedelta(hours=3)) if not post_peak_window: continue peak_bg = max(bg for _, bg in post_peak_window) peak_time = [dt for dt, bg in post_peak_window if bg == peak_bg][0] # After peak, find nadir (minimum before any sustained rise) post_peak = [(dt, bg) for dt, bg in window if dt > peak_time] if len(post_peak) < 5: continue nadir_bg = min(bg for _, bg in post_peak) nadir_time = [dt for dt, bg in post_peak if bg == nadir_bg][0] # Check if there's a rise after nadir (GNG signature) post_nadir = [(dt, bg) for dt, bg in post_peak if dt > nadir_time] if len(post_nadir) >= 3: post_nadir_avg = sum(bg for _, bg in post_nadir[:6]) / min(len(post_nadir), 6) rise = post_nadir_avg - nadir_bg hours_to_nadir = (nadir_time - last_meal_dt).total_seconds() / 3600 if hours_to_nadir > 2: # Ignore immediate post-meal dips gng_marker = "↑ GNG likely" if rise > 3 else "→ flat/unclear" print(f" {date} | Last meal: {last_meal_dt.strftime('%H:%M')} ({last_meal_food[:40]})") print(f" | Peak: {peak_bg:.0f} at {peak_time.strftime('%H:%M')} | Nadir: {nadir_bg:.0f} at {nadir_time.strftime('%H:%M')} ({hours_to_nadir:.1f}h after meal)") print(f" | Post-nadir avg: {post_nadir_avg:.1f} (Δ+{rise:.1f}) {gng_marker}") print() # ============================================================ # Q6: LAST MEAL TIMING vs OVERNIGHT GLUCOSE # ============================================================ print("\n" + "="*60) print("Q6: LAST MEAL TIMING vs OVERNIGHT GLUCOSE") print("="*60) overnight_data = [] for date, day_meals in sorted(days.items()): if not day_meals: continue last_meal_dt = max(dt for dt, _ in day_meals) last_meal_food = [f for dt, f in day_meals if dt == last_meal_dt][0] last_meal_hour = last_meal_dt.hour + last_meal_dt.minute / 60 # Get overnight glucose (midnight to 6am next day) next_day = date + timedelta(days=1) overnight_start = datetime(next_day.year, next_day.month, next_day.day, 0, 0) overnight_end = datetime(next_day.year, next_day.month, next_day.day, 6, 0) overnight = get_cgm_window(overnight_start, overnight_end) if len(overnight) < 5: continue overnight_mean = sum(bg for _, bg in overnight) / len(overnight) overnight_min = min(bg for _, bg in overnight) overnight_max = max(bg for _, bg in overnight) # Also get 4-6am specifically (dawn phenomenon window) dawn_start = datetime(next_day.year, next_day.month, next_day.day, 4, 0) dawn_end = datetime(next_day.year, next_day.month, next_day.day, 6, 0) dawn = get_cgm_window(dawn_start, dawn_end) dawn_mean = sum(bg for _, bg in dawn) / len(dawn) if dawn else None hours_before_midnight = (24 - last_meal_hour) if last_meal_hour > 12 else last_meal_hour + 24 hours_fasted_by_4am = (datetime(next_day.year, next_day.month, next_day.day, 4, 0) - last_meal_dt).total_seconds() / 3600 # Categorize last meal food_lower = last_meal_food.lower() is_carby = any(w in food_lower for w in ['rice', 'bread', 'pasta', 'noodle', 'cake', 'cookie', 'ice cream', 'chocolate', 'milk and honey', 'soda', 'fig', 'fruit', 'honey', 'corn', 'mamaliga', 'sweet', 'cobbler', 'pudding']) overnight_data.append({ 'date': date, 'last_meal_time': last_meal_dt.strftime('%H:%M'), 'last_meal_hour': last_meal_hour, 'last_meal_food': last_meal_food, 'is_carby': is_carby, 'hours_fasted_by_4am': hours_fasted_by_4am, 'overnight_mean': overnight_mean, 'overnight_min': overnight_min, 'dawn_mean': dawn_mean, }) print(f"\n{len(overnight_data)} nights with meal + overnight CGM data\n") # Sort by last meal time overnight_data.sort(key=lambda x: x['last_meal_hour']) print(f"{'Date':<12} {'Last Meal':<10} {'h fast@4am':<11} {'Night Mean':<11} {'Night Min':<10} {'Dawn 4-6':<10} {'Carby?':<7} {'Food'}") print("-" * 120) for d in overnight_data: dawn_str = f"{d['dawn_mean']:.0f}" if d['dawn_mean'] else "n/a" carby_str = "🔴" if d['is_carby'] else "🟢" print(f"{d['date']!s:<12} {d['last_meal_time']:<10} {d['hours_fasted_by_4am']:<11.1f} {d['overnight_mean']:<11.1f} {d['overnight_min']:<10.0f} {dawn_str:<10} {carby_str:<7} {d['last_meal_food'][:45]}") # Correlations if len(overnight_data) >= 5: hours = [d['hours_fasted_by_4am'] for d in overnight_data] nights = [d['overnight_mean'] for d in overnight_data] r_timing = pearson(hours, nights) print(f"\nCorrelation (hours fasted by 4am vs overnight mean): r = {r_timing:.3f}") # Carby vs non-carby last meal carby_nights = [d['overnight_mean'] for d in overnight_data if d['is_carby']] clean_nights = [d['overnight_mean'] for d in overnight_data if not d['is_carby']] if carby_nights and clean_nights: print(f"\nCarby last meal: overnight mean = {sum(carby_nights)/len(carby_nights):.1f} (n={len(carby_nights)})") print(f"Clean last meal: overnight mean = {sum(clean_nights)/len(clean_nights):.1f} (n={len(clean_nights)})") # Dawn phenomenon dawn_valid = [d for d in overnight_data if d['dawn_mean']] if len(dawn_valid) >= 5: hours_d = [d['hours_fasted_by_4am'] for d in dawn_valid] dawn_d = [d['dawn_mean'] for d in dawn_valid] r_dawn = pearson(hours_d, dawn_d) print(f"\nCorrelation (hours fasted by 4am vs dawn glucose 4-6am): r = {r_dawn:.3f}") carby_dawn = [d['dawn_mean'] for d in dawn_valid if d['is_carby']] clean_dawn = [d['dawn_mean'] for d in dawn_valid if not d['is_carby']] if carby_dawn and clean_dawn: print(f"Carby last meal → dawn mean: {sum(carby_dawn)/len(carby_dawn):.1f} (n={len(carby_dawn)})") print(f"Clean last meal → dawn mean: {sum(clean_dawn)/len(clean_dawn):.1f} (n={len(clean_dawn)})") # Early dinner vs late dinner early = [d for d in overnight_data if d['last_meal_hour'] < 19] late = [d for d in overnight_data if d['last_meal_hour'] >= 19] if early and late: print(f"\nEarly dinner (<7pm): overnight mean = {sum(d['overnight_mean'] for d in early)/len(early):.1f} (n={len(early)})") print(f"Late dinner (≥7pm): overnight mean = {sum(d['overnight_mean'] for d in late)/len(late):.1f} (n={len(late)})")