Distribution 교차점 계산

2한나·2026년 2월 5일

해당 distribution 그래프에서 각 선의 교차점을 계산해보려고 한다.

import numpy as np
import pandas as pd
from scipy.stats import gaussian_kde

def one_intersection_kde(x_succ, x_fail, n_grid=2000):
    x_succ = np.asarray(x_succ, dtype=float)
    x_fail = np.asarray(x_fail, dtype=float)

    # x 범위
    lo = min(x_succ.min(), x_fail.min())
    hi = max(x_succ.max(), x_fail.max())
    grid = np.linspace(lo, hi, n_grid)

    # KDE
    kde_s = gaussian_kde(x_succ)
    kde_f = gaussian_kde(x_fail)

    diff = kde_s(grid) - kde_f(grid)

    # 부호가 바뀌는 구간 찾기
    idx = np.where(diff[:-1] * diff[1:] < 0)[0]

    i = idx[0]
    # 선형 보간으로 x 근사
    x0, x1 = grid[i], grid[i+1]
    y0, y1 = diff[i], diff[i+1]
    x_star = x0 - y0 * (x1 - x0) / (y1 - y0)
    return float(x_star)

1. x_succ, x_fail을 numpy 배열로 변환

x_succ = np.asarray(x_succ, dtype=float)
x_fail = np.asarray(x_fail, dtype=float)
  • df = pd.read_csv("experiment_result_scores.csv")로 csv 파일을 읽어 함수를 호출하면 pandas Series 형태로 들어오므로
  • numpy 배열로 변환

2. 교점을 찾을 x축 구간을 정의 및 grid 샘플링

lo = min(x_succ.min(), x_fail.min())
hi = max(x_succ.max(), x_fail.max())
grid = np.linspace(lo, hi, n_grid)
  • 성공과 실패 두 데이터의 전체 범위를 포함하는 [lo, hi]를 x축 탐색 범위로 잡음
  • grid: 연속적인 x축을 컴퓨터가 다룰 수 있도록 이산 점들로 쪼갠 것
    • n_grid를 크게 할수록 교점 근사 정확도가 좋아짐

3. 성공/실패 각각에 대해 KDE(커널 밀도 추정) 모델을 만들기

kde_s = gaussian_kde(x_succ)
kde_f = gaussian_kde(x_fail)
  • gaussian_kde: 샘플 score들로부터 확률 밀도 함수(PDF)를 추정하는 기법
  • 결과로 얻은 kde_s, kde_f는 함수처럼 동작함
    • kde_s: x에서의 성공 분포 밀도값
    • kde_f: x에서의 실패 분초 밀도값
  • seaborn의 kde=True는 내부적으로 KDE 기반 곡선을 그리는 것이기에 교점 좌표를 얻기 위해 같은 개념을 코드로 직접 계산하는 것임

4. 두 곡선의 차이(diff) 계산

diff = kde_s(grid) - kde_f(grid)
  • 교점은 kde_s(x) == kde_f(x)인 x (y값이 같은 x좌표)
  • 이를 kde_s(x) - kde_f(x) = 0으로 바꾸면 교점을 찾을 수 있음
  • 이에 grid 위에서 diff 값을 쭉 계산해둠
    • diff > 0이면 그 구간에서는 성공 밀도가 더 큼
    • diff < 0이면 실패 밀도가 더 큼

5. diff의 부호가 바뀌는 구간 찾기 (= 교점이 있는 구간)

idx = np.where(diff[:-1] * diff[1:] < 0)[0]
  • diff는 grid 점들로 계산한 값이므로 정확히 0이 나오지 않을 수 있음
    -> 0을 지나는 구간을 찾는 방식 사용
  • 어떤 연속 함수 구간에서 값이 +에서 -로 바뀌면 그 사이에는 반드시 0이 존재함
  • diff[:-1] * diff[1:] < 0 이면 두 값의 부호가 다르다는 뜻
    -> 그 사이에 교점이 있음
    -> 해당 구간 index 저장

6. 선형 보간으로 교점 x 계산

i = idx[0]
x0, x1 = grid[i], grid[i+1]
y0, y1 = diff[i], diff[i+1]
x_star = x0 - y0 * (x1 - x0) / (y1 - y0)
  • grid[i]와 grid[i+1] 사이에 교점이 있음을 확인했지만, 그 사이에서 정확히 어디가 교점인지는 모름
    -> diff(x0)와 diff(x1) 두 점을 직선으로 연결해서 y=0일 때의 x값 계산

7. 최종 결과 반환

return float(x_star)

코드 실행 결과

Visual red-yellow: 0.5399406519498681
Visual yellow-green = 0.7175686585568468
Semantic red-green = 0.06631270799912138

=> 그래프가 정확히 교차하는 지점이 아님!

  • 이유

    • one_intersection_kde()는 scipy.stats.gaussian_kde의 기본 설정으로 KDE를 추정하는 반면, sns.histplot(..., kde=True)는 내부적으로 KDE를 계산할 때 bandwidth(스무딩), 평가 grid 해상도(gridsize), 분포 범위 확장(cut) 등의 설정이 다를 수 있음.
    • 이 설정 차이로 인해 두 KDE 곡선의 형태가 미세하게 달라지고, 교차점 위치도 약간 달라질 수 있음
  • 해결 방법

    • seaborn으로 KDE를 그린 뒤
    • ax.lines에서 그려진 KDE 곡선의 (x,y) 데이터를 뽑아서
    • 그 실제 곡선끼리 교점을 계산하기
import numpy as np

def intersection_x_from_lines(line1, line2, n_grid=20000):
    """
    line1, line2: matplotlib Line2D (ax.lines에서 가져온 KDE 선)
    """
    x1, y1 = line1.get_data()
    x2, y2 = line2.get_data()

    # 두 곡선이 실제로 겹치는 x 구간만 사용
    lo = max(np.min(x1), np.min(x2))
    hi = min(np.max(x1), np.max(x2))
    grid = np.linspace(lo, hi, n_grid)

    # 각 곡선을 같은 grid로 보간
    y1i = np.interp(grid, x1, y1)
    y2i = np.interp(grid, x2, y2)

    diff = y1i - y2i
    idx = np.where(diff[:-1] * diff[1:] < 0)[0]

    i = idx[0]
    x0, x1_ = grid[i], grid[i+1]
    d0, d1 = diff[i], diff[i+1]
    x_star = x0 - d0 * (x1_ - x0) / (d1 - d0)
    return float(x_star)

그래프 생성 함수는 다음과 같이

    sns.histplot(data=df, x="vis_success_score", kde=True, ax=ax_vis,
                 color="green", label="Success (Original-GMP)", element="step", alpha=0.3)
    line_green = ax_vis.lines[-1] 

    sns.histplot(data=df, x="vis_fail_clothes_score", kde=True, ax=ax_vis,
                 color="orange", label="Fail (Diff Clothes)", element="step", alpha=0.3)
    line_orange = ax_vis.lines[-1]

    sns.histplot(data=df, x="vis_fail_thing_score", kde=True, ax=ax_vis,
                 color="red", label="Fail (Diff Things)", element="step", alpha=0.3)
    line_red = ax_vis.lines[-1]

    # 그래프에 그려진 KDE 선 기준 교점 계산
    vis_thing_cloth_x = intersection_x_from_lines(line_red, line_orange)
    vis_success_cloth_x = intersection_x_from_lines(line_green, line_orange)

결과

Visual red-yellow: 0.5325409337303397
Visual yellow-green: 0.7290050105111927
Semantic red-green: 0.06745537268189729

=> 정확하게 그래프의 교점을 찾았다!


전체 코드

import numpy as np
import pandas as pd
from scipy.stats import gaussian_kde
import os
import sys
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from tqdm import tqdm


current_dir = os.path.dirname(os.path.abspath(__file__))
src_dir = os.path.dirname(current_dir)
if src_dir not in sys.path:
    sys.path.append(src_dir)

DATA_ROOT = Path("/home/hana/GMP/gmp_loop/data/visual_semantic_test")

# Input/Output Paths
INPUT_PATH_CSV = DATA_ROOT / "experiment_path.csv"
OUTPUT_SCORE_CSV = DATA_ROOT / "experiment_result_scores.csv"
OUTPUT_PLOT_PATH = DATA_ROOT / "score_distribution.png"
OUTPUT_PANEL_ROOT = DATA_ROOT / "panels"

# Panel Generation Settings
SCALE = 2
IMG_BOX = 384 * SCALE
TEXT_HEIGHT = 60 * SCALE
PADDING = 30 * SCALE
PID_WIDTH = 160 * SCALE
BG_COLOR = (255, 255, 255)
TEXT_COLOR = (0, 0, 0)
SCORE_COLOR_SUCCESS = (0, 100, 0)
SCORE_COLOR_FAIL = (150, 0, 0)


def one_intersection_kde(x_succ, x_fail, n_grid=10000):
    x_succ = np.asarray(x_succ, dtype=float)
    x_fail = np.asarray(x_fail, dtype=float)

    # x 범위
    lo = min(x_succ.min(), x_fail.min())
    hi = max(x_succ.max(), x_fail.max())
    grid = np.linspace(lo, hi, n_grid)

    # KDE
    kde_s = gaussian_kde(x_succ)
    kde_f = gaussian_kde(x_fail)

    diff = kde_s(grid) - kde_f(grid)

    # 부호가 바뀌는 "유일한" 구간 찾기
    idx = np.where(diff[:-1] * diff[1:] < 0)[0]

    i = idx[0]
    # 선형 보간으로 x 근사
    x0, x1 = grid[i], grid[i+1]
    y0, y1 = diff[i], diff[i+1]
    x_star = x0 - y0 * (x1 - x0) / (y1 - y0)
    return float(x_star)

    import numpy as np

def intersection_x_from_lines(line1, line2, n_grid=20000):
    """
    line1, line2: matplotlib Line2D (ax.lines에서 가져온 KDE 선)
    return: 교점 x (1개라고 가정)
    """
    x1, y1 = line1.get_data()
    x2, y2 = line2.get_data()

    # 두 곡선이 실제로 겹치는 x 구간만 사용
    lo = max(np.min(x1), np.min(x2))
    hi = min(np.max(x1), np.max(x2))
    grid = np.linspace(lo, hi, n_grid)

    # 각 곡선을 같은 grid로 보간(interpolation)
    y1i = np.interp(grid, x1, y1)
    y2i = np.interp(grid, x2, y2)

    diff = y1i - y2i
    idx = np.where(diff[:-1] * diff[1:] < 0)[0]
    if len(idx) == 0:
        raise ValueError("교점이 없습니다.")
    if len(idx) > 1:
        raise ValueError(f"교점이 1개가 아닙니다. 교점 구간 수: {len(idx)}")

    i = idx[0]
    x0, x1_ = grid[i], grid[i+1]
    d0, d1 = diff[i], diff[i+1]
    x_star = x0 - d0 * (x1_ - x0) / (d1 - d0)
    return float(x_star)


def step2_analyze_distribution():
    print("\n[Step 2] Analyzing Score Distribution...")

    if not OUTPUT_SCORE_CSV.exists():
        print("Score file not found. Run Step 1 first.")
        return False

    df = pd.read_csv(OUTPUT_SCORE_CSV).dropna(subset=[
        "vis_success_score", "vis_fail_clothes_score", "vis_fail_thing_score",
        "sem_success_score", "sem_fail_score"
    ])

    fig, axes = plt.subplots(2, 1, figsize=(12, 12))
    plt.subplots_adjust(hspace=0.3)
    sns.set_theme(style="whitegrid")

    # =========================
    # A) Visual
    # =========================
    ax_vis = axes[0]
    ax_vis.set_title("Visual Consistency Score Distribution", fontsize=15, fontweight='bold')

    sns.histplot(data=df, x="vis_success_score", kde=True, ax=ax_vis,
                 color="green", label="Success (Original-GMP)", element="step", alpha=0.3)
    line_green = ax_vis.lines[-1]   # 방금 그려진 KDE 선

    sns.histplot(data=df, x="vis_fail_clothes_score", kde=True, ax=ax_vis,
                 color="orange", label="Fail (Diff Clothes)", element="step", alpha=0.3)
    line_orange = ax_vis.lines[-1]

    sns.histplot(data=df, x="vis_fail_thing_score", kde=True, ax=ax_vis,
                 color="red", label="Fail (Diff Things)", element="step", alpha=0.3)
    line_red = ax_vis.lines[-1]

    # “그래프에 그려진 KDE 선” 기준 교점 계산
    vis_thing_cloth_x = intersection_x_from_lines(line_red, line_orange)
    vis_success_cloth_x = intersection_x_from_lines(line_green, line_orange)
    print(f"Visual red-yellow: {vis_thing_cloth_x}")
    print(f"Visual yellow-green: {vis_success_cloth_x}")

    # 교점 세로선 표시
    ax_vis.axvline(vis_thing_cloth_x, linestyle="--", linewidth=2, color="black",
                   label=f"X (thing-cloth) = {vis_thing_cloth_x:.4f}")
    ax_vis.axvline(vis_success_cloth_x, linestyle="--", linewidth=2, color="black",
                   label=f"X (succ-cloth) = {vis_success_cloth_x:.4f}")

    y_top = ax_vis.get_ylim()[1]
    ax_vis.text(vis_thing_cloth_x, y_top*0.95, f"{vis_thing_cloth_x:.4f}",
                rotation=90, va="top", ha="right", fontsize=10)
    ax_vis.text(vis_success_cloth_x, y_top*0.95, f"{vis_success_cloth_x:.4f}",
                rotation=90, va="top", ha="right", fontsize=10)

    ax_vis.legend()

    # =========================
    # B) Semantic
    # =========================
    ax_sem = axes[1]
    ax_sem.set_title("Semantic Consistency Score Distribution", fontsize=15, fontweight='bold')

    sns.histplot(data=df, x="sem_success_score", kde=True, ax=ax_sem,
                 color="green", label="Success (Correct Category)", element="step", alpha=0.3)
    line_sem_green = ax_sem.lines[-1]

    sns.histplot(data=df, x="sem_fail_score", kde=True, ax=ax_sem,
                 color="red", label="Fail (Wrong Category)", element="step", alpha=0.3)
    line_sem_red = ax_sem.lines[-1]

    # “그래프에 그려진 KDE 선” 기준 교점 계산
    sem_x = intersection_x_from_lines(line_sem_green, line_sem_red)
    print(f"Semantic red-green: {sem_x}")

    ax_sem.axvline(sem_x, linestyle="--", linewidth=2, color="black",
                   label=f"X (succ-fail) = {sem_x:.4f}")

    y_top2 = ax_sem.get_ylim()[1]
    ax_sem.text(sem_x, y_top2*0.95, f"{sem_x:.4f}",
                rotation=90, va="top", ha="right", fontsize=10)

    ax_sem.legend()

    plt.savefig(OUTPUT_PLOT_PATH, dpi=300)
    plt.close()
    print(f"Distribution plot saved to: {OUTPUT_PLOT_PATH}")
    return True

0개의 댓글