File size: 19,252 Bytes
4f26cce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
#!/usr/bin/env python3
"""Deep audit of PINO v6 dataset label quality and coverage gaps."""
import json
import numpy as np
from collections import Counter, defaultdict

DATA_PATH = "data/empirical_dataset_v6.jsonl"
VOCAB_PATH = "data/pyrfume_vocabulary.json"

with open(VOCAB_PATH) as f:
    vocab = json.load(f)
DESCRIPTOR_NAMES = vocab["vocabulary"]
N_DESCRIPTORS = vocab["dimension"]

records = []
with open(DATA_PATH) as f:
    for line in f:
        line = line.strip()
        if line:
            records.append(json.loads(line))

N = len(records)
print(f"=" * 70)
print(f"PINO v6 DATASET AUDIT — {N} total records")
print(f"=" * 70)

controls = [r for r in records if r.get("is_control")]
non_controls = [r for r in records if not r.get("is_control")]
print(f"\nControls: {len(controls)}, Non-controls: {len(non_controls)}")

genre_counts = Counter(r.get("genre") for r in records)
print(f"Genres: {dict(genre_counts)}")

# =====================================================================
# (1) PER-FORMULA INGREDIENT COUNT DISTRIBUTION
# =====================================================================
print(f"\n{'=' * 70}")
print("(1) PER-FORMULA INGREDIENT COUNT DISTRIBUTION")
print(f"{'=' * 70}")

counts_all = [len(r["formula"]) for r in records]
counts_nc = [len(r["formula"]) for r in non_controls]
counts_c = [len(r["formula"]) for r in controls]

for label, counts in [("ALL", counts_all), ("NON-CONTROL", counts_nc), ("CONTROL", counts_c)]:
    arr = np.array(counts)
    dist = Counter(counts)
    print(f"\n  [{label}] n={len(arr)}")
    print(f"    min={arr.min()}, max={arr.max()}, mean={arr.mean():.2f}, median={np.median(arr):.1f}, std={arr.std():.2f}")
    print(f"    Distribution:")
    for k in sorted(dist.keys()):
        pct = dist[k] / len(arr) * 100
        print(f"      {k} ingredients: {dist[k]} ({pct:.1f}%)")

# =====================================================================
# (2) WEIGHT FRACTION DISTRIBUTION — ETHANOL DOMINANCE
# =====================================================================
print(f"\n{'=' * 70}")
print("(2) WEIGHT FRACTION DISTRIBUTION — ETHANOL DOMINANCE")
print(f"{'=' * 70}")

ETHANOL_CAS = {"64-17-5", "25265-71-8"}

ethanol_fracs = []
non_ethanol_fracs = []
ethanol_85plus = 0
ethanol_90plus = 0
formulas_with_ethanol = 0

for r in records:
    formula = r["formula"]
    eth_frac = 0.0
    non_eth_total = 0.0
    has_eth = False
    for ing in formula:
        cas = ing["cas"]
        wf = ing["weight_fraction"]
        if cas in ETHANOL_CAS:
            eth_frac += wf
            has_eth = True
        else:
            non_eth_total += wf
    if has_eth:
        formulas_with_ethanol += 1
    ethanol_fracs.append(eth_frac)
    non_ethanol_fracs.append(non_eth_total)
    if eth_frac >= 0.85:
        ethanol_85plus += 1
    if eth_frac >= 0.90:
        ethanol_90plus += 1

eth_arr = np.array(ethanol_fracs)
print(f"\n  Ethanol (64-17-5 or 25265-71-8) weight fraction per formula:")
print(f"    Formulas containing ethanol: {formulas_with_ethanol} / {N} ({formulas_with_ethanol/N*100:.1f}%)")
print(f"    Mean ethanol fraction: {eth_arr.mean():.4f}")
print(f"    Median ethanol fraction: {np.median(eth_arr):.4f}")
print(f"    Min ethanol fraction: {eth_arr.min():.4f}")
print(f"    Max ethanol fraction: {eth_arr.max():.4f}")
print(f"    Formulas with ethanol >= 85%: {ethanol_85plus} ({ethanol_85plus/N*100:.1f}%)")
print(f"    Formulas with ethanol >= 90%: {ethanol_90plus} ({ethanol_90plus/N*100:.1f}%)")

# Distribution buckets
buckets = [0, 0.01, 0.1, 0.3, 0.5, 0.7, 0.8, 0.85, 0.9, 0.95, 0.99, 1.01]
print(f"\n    Ethanol fraction buckets:")
for i in range(len(buckets) - 1):
    lo, hi = buckets[i], buckets[i+1]
    cnt = int(np.sum((eth_arr >= lo) & (eth_arr < hi)))
    if cnt > 0:
        print(f"      [{lo:.2f}, {hi:.2f}): {cnt} ({cnt/N*100:.1f}%)")

# Per-ingredient weight fraction stats (non-ethanol)
print(f"\n  Per-ingredient weight fraction (excluding ethanol):")
all_ingredient_fracs = []
for r in records:
    for ing in r["formula"]:
        if ing["cas"] not in ETHANOL_CAS:
            all_ingredient_fracs.append(ing["weight_fraction"])
if all_ingredient_fracs:
    ing_arr = np.array(all_ingredient_fracs)
    print(f"    n={len(ing_arr)} ingredient-slots")
    print(f"    mean={ing_arr.mean():.4f}, median={np.median(ing_arr):.4f}")
    print(f"    min={ing_arr.min():.6f}, max={ing_arr.max():.4f}")
    print(f"    percentiles: 10%={np.percentile(ing_arr,10):.4f}, 25%={np.percentile(ing_arr,25):.4f}, "
          f"75%={np.percentile(ing_arr,75):.4f}, 90%={np.percentile(ing_arr,90):.4f}")

# =====================================================================
# (3) PYRAMID TIER COVERAGE — formulas with at least one ingredient per tier
# =====================================================================
print(f"\n{'=' * 70}")
print("(3) PYRAMID TIER COVERAGE (top/mid/base)")
print(f"{'=' * 70}")
# pyramid_targets is 3 lists of 138 binary values
# Tier means at least one descriptor active in that tier

has_top = 0
has_mid = 0
has_base = 0
has_all_three = 0
has_none = 0
tier_sums = []

for r in records:
    pt = r.get("pyramid_targets")
    if pt is None:
        continue
    # Each tier is a list of 138 values
    top_active = sum(1 for x in pt[0] if x > 0)
    mid_active = sum(1 for x in pt[1] if x > 0)
    base_active = sum(1 for x in pt[2] if x > 0)
    tier_sums.append((top_active, mid_active, base_active))
    
    t_has = top_active > 0
    m_has = mid_active > 0
    b_has = base_active > 0
    if t_has: has_top += 1
    if m_has: has_mid += 1
    if b_has: has_base += 1
    if t_has and m_has and b_has: has_all_three += 1
    if not (t_has or m_has or b_has): has_none += 1

total_with_pyramid = len(tier_sums)
print(f"\n  Formulas with pyramid_targets: {total_with_pyramid}")
print(f"  Has top notes active: {has_top} ({has_top/total_with_pyramid*100:.1f}%)")
print(f"  Has mid notes active: {has_mid} ({has_mid/total_with_pyramid*100:.1f}%)")
print(f"  Has base notes active: {has_base} ({has_base/total_with_pyramid*100:.1f}%)")
print(f"  Has ALL THREE tiers active: {has_all_three} ({has_all_three/total_with_pyramid*100:.1f}%)")
print(f"  Has NO tiers active (all-zero): {has_none} ({has_none/total_with_pyramid*100:.1f}%)")

tier_arr = np.array(tier_sums)
print(f"\n  Avg active descriptors per tier:")
print(f"    Top:  {tier_arr[:,0].mean():.2f} (min={tier_arr[:,0].min()}, max={tier_arr[:,0].max()})")
print(f"    Mid:  {tier_arr[:,1].mean():.2f} (min={tier_arr[:,1].min()}, max={tier_arr[:,1].max()})")
print(f"    Base: {tier_arr[:,2].mean():.2f} (min={tier_arr[:,2].min()}, max={tier_arr[:,2].max()})")

# Also check only non-controls
has_all_nc = 0
for r in non_controls:
    pt = r.get("pyramid_targets")
    if pt and sum(1 for x in pt[0] if x>0) > 0 and sum(1 for x in pt[1] if x>0) > 0 and sum(1 for x in pt[2] if x>0) > 0:
        has_all_nc += 1
print(f"\n  [Non-controls only] Has ALL THREE tiers: {has_all_nc} / {len(non_controls)} ({has_all_nc/len(non_controls)*100:.1f}%)")

# =====================================================================
# (4) CHEMICAL FREQUENCY — over-represented chemicals
# =====================================================================
print(f"\n{'=' * 70}")
print("(4) CHEMICAL FREQUENCY / OVER-REPRESENTATION")
print(f"{'=' * 70}")

cas_counter = Counter()
cas_name = {}
for r in records:
    for ing in r["formula"]:
        cas = ing["cas"]
        cas_counter[cas] += 1
        if cas not in cas_name and ing.get("name"):
            cas_name[cas] = ing["name"]

print(f"\n  Unique chemicals (by CAS/SMILES key): {len(cas_counter)}")
print(f"\n  Top 30 most frequent chemicals:")
print(f"  {'Rank':>4} {'CAS':<20} {'Name':<30} {'Count':>6} {'% of formulas':>14}")
for i, (cas, cnt) in enumerate(cas_counter.most_common(30)):
    name = cas_name.get(cas, "(no name)")
    # Check if it's a SMILES key
    display_cas = cas[:18] + ".." if len(cas) > 20 else cas
    display_name = name[:28] + ".." if len(name) > 30 else name
    print(f"  {i+1:>4} {display_cas:<20} {display_name:<30} {cnt:>6} {cnt/N*100:>13.1f}%")

print(f"\n  Chemicals appearing in >50% of formulas: {sum(1 for _,c in cas_counter.items() if c/N > 0.5)}")
print(f"  Chemicals appearing in >25% of formulas: {sum(1 for _,c in cas_counter.items() if c/N > 0.25)}")
print(f"  Chemicals appearing in >10% of formulas: {sum(1 for _,c in cas_counter.items() if c/N > 0.10)}")
print(f"  Chemicals appearing in >5% of formulas:  {sum(1 for _,c in cas_counter.items() if c/N > 0.05)}")
print(f"  Chemicals appearing in only 1 formula:   {sum(1 for _,c in cas_counter.items() if c == 1)}")
print(f"  Chemicals appearing in <=2 formulas:     {sum(1 for _,c in cas_counter.items() if c <= 2)}")

# Also check non-controls only
cas_counter_nc = Counter()
for r in non_controls:
    for ing in r["formula"]:
        cas_counter_nc[ing["cas"]] += 1
print(f"\n  [Non-controls only] Top 15:")
for i, (cas, cnt) in enumerate(cas_counter_nc.most_common(15)):
    name = cas_name.get(cas, "")
    display_cas = cas[:18] + ".." if len(cas) > 20 else cas
    print(f"  {i+1:>4} {display_cas:<20} {cnt:>5} / {len(non_controls)} ({cnt/len(non_controls)*100:.1f}%)  {name[:40]}")

# =====================================================================
# (5) DESCRIPTOR CO-OCCURRENCE — redundancy analysis
# =====================================================================
print(f"\n{'=' * 70}")
print("(5) DESCRIPTOR CO-OCCURRENCE / REDUNDANCY")
print(f"{'=' * 70}")
# Use pyramid_targets flattened across all 3 tiers to find descriptor activity
# Actually, use objective_targets which is per-timestep (49 × 138)
# But for descriptor co-occurrence, aggregate: a descriptor is "active" in a formula
# if it's non-zero in any tier of pyramid_targets OR in objective_targets

# Use pyramid_targets: flatten 3×138 → 138 (OR across tiers)
descriptor_active = []  # per formula: set of active descriptor indices
for r in records:
    pt = r.get("pyramid_targets")
    if pt is None:
        descriptor_active.append(set())
        continue
    active = set()
    for tier in pt:
        for idx, val in enumerate(tier):
            if val > 0:
                active.add(idx)
    descriptor_active.append(active)

# Co-occurrence: count pairs
pair_counts = Counter()
single_counts = Counter()
for active_set in descriptor_active:
    active_sorted = sorted(active_set)
    for idx in active_sorted:
        single_counts[idx] += 1
    for i in range(len(active_sorted)):
        for j in range(i+1, len(active_sorted)):
            pair_counts[(active_sorted[i], active_sorted[j])] += 1

# Find always-together pairs: P(B|A) and P(A|B) both near 1.0
print(f"\n  Descriptor pairs with near-perfect co-occurrence (both directions >=0.95):")
perfect_pairs = []
for (a, b), co_occur in pair_counts.most_common():
    ca = single_counts[a]
    cb = single_counts[b]
    if ca == 0 or cb == 0:
        continue
    p_b_given_a = co_occur / ca
    p_a_given_b = co_occur / cb
    if p_b_given_a >= 0.95 and p_a_given_b >= 0.95 and co_occur >= 5:
        perfect_pairs.append((a, b, co_occur, p_b_given_a, p_a_given_b))
        
if perfect_pairs:
    print(f"    Found {len(perfect_pairs)} pairs:")
    for a, b, co, pba, pab in perfect_pairs[:30]:
        print(f"      [{DESCRIPTOR_NAMES[a]:>15}] ↔ [{DESCRIPTOR_NAMES[b]:>15}]  co={co:>5}  P(B|A)={pba:.3f}  P(A|B)={pab:.3f}")
else:
    print("    None found.")

# High co-occurrence pairs (>= 0.90)
print(f"\n  Descriptor pairs with high co-occurrence (both >=0.90, co-occur >=10):")
high_pairs = []
for (a, b), co_occur in pair_counts.most_common():
    ca = single_counts[a]
    cb = single_counts[b]
    if ca == 0 or cb == 0:
        continue
    p_b_given_a = co_occur / ca
    p_a_given_b = co_occur / cb
    if p_b_given_a >= 0.90 and p_a_given_b >= 0.90 and co_occur >= 10:
        high_pairs.append((a, b, co_occur, p_b_given_a, p_a_given_b))
print(f"    Found {len(high_pairs)} pairs:")
for a, b, co, pba, pab in high_pairs[:30]:
    print(f"      [{DESCRIPTOR_NAMES[a]:>15}] ↔ [{DESCRIPTOR_NAMES[b]:>15}]  co={co:>5}  P(B|A)={pba:.3f}  P(A|B)={pab:.3f}")

# Top 20 most co-occurring pairs by raw count
print(f"\n  Top 20 descriptor pairs by raw co-occurrence count:")
for (a, b), co in pair_counts.most_common(20):
    ca, cb = single_counts[a], single_counts[b]
    pba = co / ca if ca else 0
    pab = co / cb if cb else 0
    print(f"      [{DESCRIPTOR_NAMES[a]:>15}] + [{DESCRIPTOR_NAMES[b]:>15}]  co={co:>5}  P(B|A)={pba:.3f}  P(A|B)={pab:.3f}")

# =====================================================================
# (6) PER-DESCRIPTOR POSITIVE COUNT
# =====================================================================
print(f"\n{'=' * 70}")
print("(6) PER-DESCRIPTOR POSITIVE COUNT")
print(f"{'=' * 70}")

# Count: how many formulas have each descriptor active (in any tier)
desc_pos_counts = np.zeros(N_DESCRIPTORS, dtype=int)
for active_set in descriptor_active:
    for idx in active_set:
        desc_pos_counts[idx] += 1

print(f"\n  Total descriptors: {N_DESCRIPTORS}")
print(f"  Descriptors with 0 positives: {int(np.sum(desc_pos_counts == 0))}")
print(f"  Descriptors with <10 positives: {int(np.sum(desc_pos_counts < 10))}")
print(f"  Descriptors with <50 positives: {int(np.sum(desc_pos_counts < 50))}")
print(f"  Descriptors with <100 positives: {int(np.sum(desc_pos_counts < 100))}")
print(f"  Descriptors with >=100 positives: {int(np.sum(desc_pos_counts >= 100))}")
print(f"  Descriptors with >=500 positives: {int(np.sum(desc_pos_counts >= 500))}")

print(f"\n  All {N_DESCRIPTORS} descriptors sorted by positive count:")
print(f"  {'Idx':>4} {'Descriptor':<18} {'Positives':>9} {'% of N':>8}")
sorted_descs = sorted(range(N_DESCRIPTORS), key=lambda i: desc_pos_counts[i])
for i in sorted_descs:
    cnt = desc_pos_counts[i]
    name = DESCRIPTOR_NAMES[i] if i < len(DESCRIPTOR_NAMES) else f"idx_{i}"
    flag = " *** ZERO" if cnt == 0 else (" ** <10" if cnt < 10 else (" * <50" if cnt < 50 else ""))
    print(f"  {i:>4} {name:<18} {cnt:>9} {cnt/N*100:>7.1f}%{flag}")

# Per-tier descriptor counts
print(f"\n  Per-tier descriptor positive counts (pyramid_targets):")
for tier_idx, tier_name in enumerate(["TOP", "MID", "BASE"]):
    tier_counts = np.zeros(N_DESCRIPTORS, dtype=int)
    for r in records:
        pt = r.get("pyramid_targets")
        if pt and len(pt) > tier_idx:
            for d_idx, val in enumerate(pt[tier_idx]):
                if val > 0:
                    tier_counts[d_idx] += 1
    print(f"\n    [{tier_name}] Descriptors with 0 positives in this tier: {int(np.sum(tier_counts == 0))}")
    print(f"    [{tier_name}] Descriptors with <10 positives: {int(np.sum(tier_counts < 10))}")
    print(f"    [{tier_name}] Top 10: ", end="")
    top10 = sorted(range(N_DESCRIPTORS), key=lambda i: tier_counts[i], reverse=True)[:10]
    print(", ".join(f"{DESCRIPTOR_NAMES[i]}({tier_counts[i]})" for i in top10))

# =====================================================================
# (7) PSYCHOMETRIC TARGET DISTRIBUTION
# =====================================================================
print(f"\n{'=' * 70}")
print("(7) PSYCHOMETRIC TARGET DISTRIBUTION")
print(f"{'=' * 70}")

psy_records = [r for r in records if r.get("psychometric_targets") is not None]
print(f"\n  Records with psychometric_targets: {len(psy_records)}")

if psy_records:
    sample = psy_records[0]["psychometric_targets"]
    print(f"  Dimensionality: {len(sample)}")
    
    psy_arr = np.array([r["psychometric_targets"] for r in psy_records])
    
    # Per-dimension stats
    print(f"\n  Per-dimension statistics:")
    dim_names = ["season_0 (winter)", "season_1 (spring)", "season_2 (summer)", 
                 "season_3 (autumn)", "gender", "wear_day", "wear_night"]
    for d in range(min(len(sample), 7)):
        name = dim_names[d] if d < len(dim_names) else f"dim_{d}"
        col = psy_arr[:, d]
        print(f"    {name}: mean={col.mean():.4f}, std={col.std():.4f}, "
              f"min={col.min():.4f}, max={col.max():.4f}")
        
        # Check unique values
        unique_vals, counts = np.unique(col, return_counts=True)
        if len(unique_vals) <= 20:
            val_str = ", ".join(f"{v:.2f}({c})" for v, c in zip(unique_vals, counts))
            print(f"      Unique values: {val_str}")
        else:
            # Histogram
            percentiles = [0, 10, 25, 50, 75, 90, 100]
            print(f"      Percentiles: {', '.join(f'{p}%={np.percentile(col,p):.3f}' for p in percentiles)}")
    
    # Check if any dimension is constant
    print(f"\n  Constant dimensions (std < 0.01):")
    for d in range(min(len(sample), 7)):
        name = dim_names[d] if d < len(dim_names) else f"dim_{d}"
        col = psy_arr[:, d]
        if col.std() < 0.01:
            print(f"    {name}: CONSTANT at {col.mean():.4f}")
    
    # Seasonality: which season is highest per formula
    if len(sample) >= 4:
        seasons = psy_arr[:, :4]
        dominant_season = np.argmax(seasons, axis=1)
        season_names = ["winter", "spring", "summer", "autumn"]
        print(f"\n  Dominant season distribution:")
        for s in range(4):
            cnt = int(np.sum(dominant_season == s))
            print(f"    {season_names[s]}: {cnt} ({cnt/len(psy_arr)*100:.1f}%)")
    
    # Gender distribution
    if len(sample) >= 5:
        gender = psy_arr[:, 4]
        print(f"\n  Gender (dim 4):")
        unique_vals, counts = np.unique(np.round(gender, 2), return_counts=True)
        for v, c in zip(unique_vals, counts):
            print(f"    {v:.2f}: {c} ({c/len(gender)*100:.1f}%)")
    
    # Wear day/night
    if len(sample) >= 7:
        wear_day = psy_arr[:, 5]
        wear_night = psy_arr[:, 6]
        print(f"\n  Wear day (dim 5):")
        unique_vals, counts = np.unique(np.round(wear_day, 2), return_counts=True)
        for v, c in zip(unique_vals, counts):
            print(f"    {v:.2f}: {c} ({c/len(wear_day)*100:.1f}%)")
        print(f"\n  Wear night (dim 6):")
        unique_vals, counts = np.unique(np.round(wear_night, 2), return_counts=True)
        for v, c in zip(unique_vals, counts):
            print(f"    {v:.2f}: {c} ({c/len(wear_night)*100:.1f}%)")
        
        # Day vs night
        day_gt_night = int(np.sum(wear_day > wear_night))
        night_gt_day = int(np.sum(wear_night > wear_day))
        equal = int(np.sum(np.abs(wear_day - wear_night) < 0.01))
        print(f"\n  Day > Night: {day_gt_night}, Night > Day: {night_gt_day}, Equal: {equal}")

# Psychometric by genre
print(f"\n  Psychometric means by genre:")
for genre in sorted(set(r.get("genre") for r in records)):
    g_records = [r for r in psy_records if r.get("genre") == genre]
    if g_records:
        g_arr = np.array([r["psychometric_targets"] for r in g_records])
        means = g_arr.mean(axis=0)
        dim_strs = []
        for d in range(min(7, len(means))):
            name = dim_names[d] if d < len(dim_names) else f"d{d}"
            dim_strs.append(f"{name}={means[d]:.3f}")
        print(f"    [{genre}] n={len(g_records)}: {', '.join(dim_strs)}")

print(f"\n{'=' * 70}")
print("AUDIT COMPLETE")
print(f"{'=' * 70}")