| |
| """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)}") |
|
|
| |
| |
| |
| 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}%)") |
|
|
| |
| |
| |
| 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}%)") |
|
|
| |
| 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}%)") |
|
|
| |
| 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}") |
|
|
| |
| |
| |
| print(f"\n{'=' * 70}") |
| print("(3) PYRAMID TIER COVERAGE (top/mid/base)") |
| print(f"{'=' * 70}") |
| |
| |
|
|
| 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 |
| |
| 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()})") |
|
|
| |
| 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}%)") |
|
|
| |
| |
| |
| 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)") |
| |
| 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)}") |
|
|
| |
| 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]}") |
|
|
| |
| |
| |
| print(f"\n{'=' * 70}") |
| print("(5) DESCRIPTOR CO-OCCURRENCE / REDUNDANCY") |
| print(f"{'=' * 70}") |
| |
| |
| |
| |
|
|
| |
| descriptor_active = [] |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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.") |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| |
| |
| print(f"\n{'=' * 70}") |
| print("(6) PER-DESCRIPTOR POSITIVE COUNT") |
| print(f"{'=' * 70}") |
|
|
| |
| 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}") |
|
|
| |
| 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)) |
|
|
| |
| |
| |
| 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]) |
| |
| |
| 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}") |
| |
| |
| 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: |
| |
| percentiles = [0, 10, 25, 50, 75, 90, 100] |
| print(f" Percentiles: {', '.join(f'{p}%={np.percentile(col,p):.3f}' for p in percentiles)}") |
| |
| |
| 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}") |
| |
| |
| 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}%)") |
| |
| |
| 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}%)") |
| |
| |
| 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_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}") |
|
|
| |
| 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}") |
|
|