This page was generated from a Jupyter notebook. Check the source code or download the notebook..

将眼底照片特定于患者的映射到三维眼成像 - 第2部分:分析#

该示例包含对论文的光线追迹结果的分析Patient-specific mapping of fundus photographs to three-dimensional ocular imaging的分析,并将所提出的方法与其他的眼底映射方法进行比较。

引用#

除引用ZOSPy外,还请在使用此示例或此示例中提供的数据时引用以下论文:

Haasjes, C., Vu, T. H. K., & Beenakker, J.-W. M. (2024). Patient-specific mapping of fundus photographs to three-dimensional ocular imaging. Medical Physics. https://doi.org/10.1002/mp.17576

保修和责任#

提供的代码和数据仅用于研究目的。没有保证,也不能从中获得权利,正如该存储库的一般许可中所述。

导入依赖项#

[1]:
from __future__ import annotations

import json

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from helpers import (
    _upper_ellipse,
    ellipse_arc_length,
    euclidean_distance,
    find_ellipse_intersection,
)
from scipy.optimize import curve_fit
[2]:
import warnings

warnings.filterwarnings("ignore", message="invalid value encountered in sqrt")

由rayTracing.ipynb生成的受试者的加载数据。此外,加载了Navarro眼模型的参考数据。

[3]:
with open("data/navarro_geometry.json") as f:
    navarro_geometry = json.load(f)

with open("data/geometry.json") as f:
    patient_geometry = json.load(f)

with open("data/geometry_lamberth.json") as f:
    patient_geometry_lamberth = json.load(f)

navarro_ray_trace_data = pd.read_csv("data/navarro_ray_trace_results.csv")
navarro_input_output_angles = pd.read_csv("data/navarro_input_output_angles.csv")

ray_trace_data = pd.read_csv("data/ray_trace_results.csv")
ray_trace_data_lamberth = pd.read_csv("data/ray_trace_results_lamberth.csv")
input_output_angles = pd.read_csv("data/input_output_angles.csv")
input_output_angles_lamberth = pd.read_csv("data/input_output_angles_lamberth.csv")


# Parse tuples in the retina_location column
for df in [
    navarro_input_output_angles,
    input_output_angles,
    input_output_angles_lamberth,
]:
    df.retina_location = df.retina_location.apply(eval)

绘制Navarro眼模型(实线)和患者特异性眼模(虚线)的相机角度和视网膜角度之间的关系。

[4]:
from matplotlib.lines import Line2D

fig, ax = plt.subplots()

for df, ls in zip([navarro_input_output_angles, input_output_angles], ["-", "--"]):
    sns.lineplot(
        data=df,
        x="input_angle_field",
        y="output_angle_np2",
        ls=ls,
        color="tab:blue",
        label="$2^{\\mathrm{nd}}$ nodal point",
    )
    sns.lineplot(
        data=df,
        x="input_angle_field",
        y="output_angle_retina_center",
        ls=ls,
        color="tab:orange",
        label="Retina center",
    )
    sns.lineplot(
        data=df,
        x="input_angle_field",
        y="output_angle_pupil",
        ls=ls,
        color="tab:green",
        label="Pupil",
    )

# Edit legend
handles = ax.get_legend_handles_labels()[0][:3]
handles += [
    Line2D([], [], linestyle="-", color="black", label="Navarro"),
    Line2D([], [], linestyle="--", color="black", label="Patient"),
]
ax.legend(handles=handles, loc="upper left")

ax.set_xlabel("Camera angle [°]")
ax.set_ylabel("Retina angle [°]")
ax.set_aspect("equal")
ax.grid()
../../_images/examples_%E5%9F%BA%E4%BA%8E%E6%82%A3%E8%80%85%E4%B8%AA%E4%BD%93%E7%89%B9%E5%BE%81%E7%9A%84%E7%9C%BC%E5%BA%95%E7%85%A7%E7%89%87%E4%B8%8E%E4%B8%89%E7%BB%B4%E7%9C%BC%E9%83%A8%E5%BD%B1%E5%83%8F%E9%85%8D%E5%87%86%E7%A0%94%E7%A9%B6_2_analysis_7_0.png

摄像机角度和视网膜角度之间的关系#

在Navarro Eye的光线追迹数据上,拟合不同参考点之间的摄像机角度和视网膜角度之间的线性关系。 然后,这些拟合被用作非光线追迹方法的参考,以从摄像头角度预测视网膜角度。 由于视网膜中心和通孔观察到的非线性高于40°,因此在高达40°的摄像头角度进行拟合。

[5]:
# Fit nodal point method on Navarro data
fit_input_output_angles = navarro_input_output_angles.query("input_angle_field <= 40")

(c1_np2,), _ = curve_fit(
    lambda theta, c1: c1 * theta,
    xdata=fit_input_output_angles.input_angle_field,
    ydata=fit_input_output_angles.output_angle_np2,
    p0=1,
)

(c1_retina_center,), _ = curve_fit(
    lambda theta, c1: c1 * theta,
    xdata=fit_input_output_angles.input_angle_field,
    ydata=fit_input_output_angles.output_angle_retina_center,
    p0=1,
)

(c1_pupil,), _ = curve_fit(
    lambda theta, c1: c1 * theta,
    xdata=fit_input_output_angles.input_angle_field,
    ydata=fit_input_output_angles.output_angle_pupil,
    p0=1,
)

print(f"NP2 fit            : {c1_np2:.3f}")
print(f"Retinal center fit : {c1_retina_center:.3f}")
print(f"Pupil fit          : {c1_pupil:.3f}")
NP2 fit            : 0.999
Retinal center fit : 1.354
Pupil fit          : 0.804

将拟合关系与其他方法进行比较#

使用使用Navarro Eye模型获得的拟合关系,以确定模拟受试者的相应视网膜位置而无需光线追迹。本课题前期获得的光线追踪数据作为基础真值。

参考点方法#

使用三个参考点之一(第二节点点,视网膜中心和学生)确定视网膜位置

[6]:
input_output_angles["output_angle_np2_fit"] = c1_np2 * input_output_angles.input_angle_field
input_output_angles["output_angle_retina_center_fit"] = c1_retina_center * input_output_angles.input_angle_field
input_output_angles["output_angle_pupil_fit"] = c1_pupil * input_output_angles.input_angle_field

# Calculate retinal locations
input_output_angles["retina_location_np2"] = [
    find_ellipse_intersection(
        r.location_np2,
        np.deg2rad(r.output_angle_np2_fit),
        patient_geometry["retina_radius_z"],
        patient_geometry["retina_radius_y"],
        r.location_retina_center,
    )
    for r in input_output_angles.itertuples()
]

input_output_angles["retina_location_retina_center"] = [
    find_ellipse_intersection(
        r.location_retina_center,
        np.deg2rad(r.output_angle_retina_center_fit),
        patient_geometry["retina_radius_z"],
        patient_geometry["retina_radius_y"],
        r.location_retina_center,
    )
    for r in input_output_angles.itertuples()
]

input_output_angles["retina_location_pupil"] = [
    find_ellipse_intersection(
        0,
        np.deg2rad(r.output_angle_pupil_fit),
        patient_geometry["retina_radius_z"],
        patient_geometry["retina_radius_y"],
        r.location_retina_center,
    )
    for r in input_output_angles.itertuples()
]

input_output_angles["distance_np2"] = euclidean_distance(
    input_output_angles.retina_location, input_output_angles.retina_location_np2
)

input_output_angles["distance_retina_center"] = euclidean_distance(
    input_output_angles.retina_location,
    input_output_angles.retina_location_retina_center,
)

input_output_angles["distance_pupil"] = euclidean_distance(
    input_output_angles.retina_location, input_output_angles.retina_location_pupil
)

Eyeplan#

通过Eyeplan中使用的方法确定摄像头角度和视网膜角度之间的关系。

[7]:
OPTIC_FIT_FACTOR = 0.126
FIELD_OF_VIEW = 53.4  # degrees
FILM_SIZE = 1  # cm


def inverse_eyeplan_formula(camera_angle: float, fov: float = FIELD_OF_VIEW, off: float = OPTIC_FIT_FACTOR) -> float:
    """Map `camera_angle` to a retinal angle according to EYEPLAN."""
    return camera_angle * fov / (fov - camera_angle * off)


# EYEPLAN defines a "nodal point" at 3.5 mm behind the cornea
input_output_angles["location_np_eyeplan"] = 3.5 - (
    patient_geometry["cornea_thickness"] + patient_geometry["anterior_chamber_depth"]
)
input_output_angles["output_angle_eyeplan_formula"] = inverse_eyeplan_formula(input_output_angles.input_angle_field)

# Calculate retinal locations according to EYEPLAN
input_output_angles["retina_location_eyeplan"] = [
    find_ellipse_intersection(
        r.location_np_eyeplan,
        np.deg2rad(r.output_angle_eyeplan_formula),
        patient_geometry["retina_radius_z"],
        patient_geometry["retina_radius_y"],
        r.location_retina_center,
    )
    for r in input_output_angles.itertuples()
]


input_output_angles["distance_eyeplan"] = euclidean_distance(
    input_output_angles.retina_location, input_output_angles.retina_location_eyeplan
)

Corcoran#

Corcoran等人提出的公式,用于(早期版本的)Optos眼底镜。

[8]:
def corcoran_formula(
    external_angle: float,
    m: float = 0.819,
    R: float = 12,  # noqa: N803
    x: float = 3.68,
) -> float:
    """Corcoran (Optos) mapping formula.

    Converts an external angle (camera angle) to an internal angle (retinal angle) w.r.t. retina center using the
    Corcoran formula.
    """
    external_angle_rad = np.deg2rad(external_angle)

    internal_angle = np.rad2deg(
        m * external_angle_rad + 2 * np.arcsin((R - x) / R * np.sin(m * external_angle_rad / 2))
    )

    return internal_angle


input_output_angles["output_angle_corcoran"] = corcoran_formula(input_output_angles.input_angle_field)

# Calculate retinal locations according to Corcoran formula
input_output_angles["retina_location_corcoran"] = [
    find_ellipse_intersection(
        r.location_retina_center,
        np.deg2rad(r.output_angle_corcoran),
        patient_geometry["retina_radius_z"],
        patient_geometry["retina_radius_y"],
        r.location_retina_center,
    )
    for r in input_output_angles.itertuples()
]

input_output_angles["distance_corcoran"] = euclidean_distance(
    input_output_angles.retina_location, input_output_angles.retina_location_corcoran
)

以下投影方法的工作与上述方法略有不同,因为它们采用笛卡尔坐标而不是角度作为输入。 这需要在输入角度和图像坐标之间进行附加的转换步骤。这种转换的常数是通过拟合Navarro Eye模型的光线追迹数据获得的。

Lamberth方位角相等的投影#

[9]:
def lamberth_image_to_retina_coordinate(
    y_image: float, r: float = 1, z_retina_center: float = 0
) -> tuple[float, float]:
    """
    Convert an image coordinate to a retinal location using the Lamberth Azimuthal Equal-Area projection.

    Parameters
    ----------
    y_image : float
        Image coordinate.
    r : float
        Radius of the retina. Only spheres are supported.

    Returns
    -------
    tuple[float, float]
        Axial and radial retinal coordinates.
    """
    # Lamberth projection uses coordinates on the unit sphere
    y_retina_norm = np.sqrt(1 - y_image**2 / 4) * y_image
    z_retina_norm = -1 + y_image**2 / 2

    y_retina = y_retina_norm * r
    z_retina = z_retina_norm * r

    # Flip the z-axis: otherwise the back of the retina will get a negative z-coordinate
    return -1 * z_retina + z_retina_center, y_retina


def lamberth_retina_to_image_coordinate(
    z_retina: float, y_retina: float, r: float = 1, z_retina_center: float = 0
) -> float:
    """
    Convert a retinal location to an image coordinate using the Lamberth Azimuthal Equal-Area projection.

    Parameters
    ----------
    z_retina : float
        Axial retinal coordinate.
    y_retina : float
        Radial retinal coordinate.
    r : float
        Radius of the retina. Only spheres are supported.

    Returns
    -------
    float
        Image coordinate.
    """
    y_retina_norm = y_retina / r
    z_retina_norm = (z_retina - z_retina_center) / r

    y_image = np.sqrt(2 / (1 + z_retina_norm)) * y_retina_norm

    return y_image


assert np.isclose(
    0.5,
    lamberth_retina_to_image_coordinate(*lamberth_image_to_retina_coordinate(0.5)),
), "Projection roundtrip fails."
[10]:
def lamberth_angle_conversion_factor(angle: float = 5) -> float:
    """Calculate a scale factor to convert from a camera angle to a Lamberth projection image coordinate.

    Image coordinates are in 'Lamberth projection space'. The Lamberth projection is defined on the unit
    sphere, so all projected images have the same size.

    Parameters
    ----------
    angle : float
        Angle for which the ray trace result is used to calculate the conversion factor.

    Returns
    -------
    float
        Conversion factor in millimeters / degree.
    """
    geometry = navarro_geometry
    ray_trace_data = navarro_ray_trace_data

    mean_retinal_radius = (geometry["retina_radius_y"] + geometry["retina_radius_z"]) / 2
    retina_center = geometry["axial_length"] - (
        geometry["cornea_thickness"] + geometry["anterior_chamber_depth"] + mean_retinal_radius
    )

    retina_coordinate = ray_trace_data.query("Surf == '7' and InputAngle == @angle").iloc[0][
        ["Z-coordinate", "Y-coordinate"]
    ]

    image_coordinate = lamberth_retina_to_image_coordinate(*retina_coordinate, mean_retinal_radius, retina_center)

    return image_coordinate / angle
[11]:
input_output_angles_lamberth["lamberth_angle_conversion_factor"] = lamberth_angle_conversion_factor()

input_output_angles_lamberth["lamberth_projected_image_size"] = (
    input_output_angles_lamberth.lamberth_angle_conversion_factor * input_output_angles_lamberth.input_angle_field
)

input_output_angles_lamberth["retina_location_lamberth"] = input_output_angles_lamberth.apply(
    lambda r: lamberth_image_to_retina_coordinate(
        r.lamberth_projected_image_size,
        r=abs(patient_geometry_lamberth["retina_curvature"]),
        z_retina_center=r.location_retina_center,
    ),
    axis=1,
)

input_output_angles_lamberth["distance_lamberth"] = euclidean_distance(
    input_output_angles_lamberth.retina_location,
    input_output_angles_lamberth.retina_location_lamberth,
)

input_output_angles[["distance_lamberth", "retina_location_lamberth"]] = input_output_angles_lamberth[
    ["distance_lamberth", "retina_location_lamberth"]
]

等距投影#

[12]:
from scipy.optimize import minimize_scalar


def octopus_image_to_retina_coordinate(y_image: float, geometry: dict[str, float | int]) -> tuple[float, float]:
    solve_z = minimize_scalar(
        lambda z: abs(
            ellipse_arc_length(
                x1=z,
                x2=geometry["retina_radius_z"],
                r_x=geometry["retina_radius_z"],
                r_y=geometry["retina_radius_y"],
            )
            - y_image
        ),
        bounds=(-geometry["retina_radius_z"], geometry["retina_radius_z"]),
    )

    if not solve_z.success:
        raise RuntimeError(f"Could not solve coordinate for arc length {y_image=}.")

    z_retina = solve_z.x

    y_retina = _upper_ellipse(
        z_retina,
        r_x=geometry["retina_radius_z"],
        r_y=geometry["retina_radius_y"],
    )

    z_retina_center = geometry["lens_thickness"] + geometry["vitreous_thickness"] - geometry["retina_radius_z"]
    z_retina += z_retina_center

    return z_retina, y_retina


def octopus_retina_to_image_coordinate(z_retina: float, y_retina: float, geometry: dict[str, float | int]) -> float:
    z_retina_center = geometry["lens_thickness"] + geometry["vitreous_thickness"] - geometry["retina_radius_z"]

    z_retina -= z_retina_center

    assert np.isclose(
        _upper_ellipse(
            z_retina,
            r_x=geometry["retina_radius_z"],
            r_y=geometry["retina_radius_y"],
        ),
        y_retina,
    )

    arc_length = ellipse_arc_length(
        z_retina,
        geometry["retina_radius_z"],
        r_x=geometry["retina_radius_z"],
        r_y=geometry["retina_radius_y"],
    )

    return arc_length


assert np.isclose(
    octopus_retina_to_image_coordinate(
        *octopus_image_to_retina_coordinate(2 * np.pi * 12 / 8, navarro_geometry),
        navarro_geometry,
    ),
    2 * np.pi * 12 / 8,
), "Projection roundtrip fails."
[13]:
def octopus_angle_conversion_factor(angle: float = 5) -> float:
    """Calculate a scale factor to convert from a camera angle to a Lamberth projection image coordinate."""
    retina_coordinate = navarro_ray_trace_data.query("Surf == '7' and InputAngle == @angle").iloc[0][
        ["Z-coordinate", "Y-coordinate"]
    ]

    image_coordinate = octopus_retina_to_image_coordinate(*retina_coordinate, navarro_geometry)

    return image_coordinate / angle
[14]:
input_output_angles["polar_angle_conversion_factor"] = octopus_angle_conversion_factor()

input_output_angles["polar_projected_image_size"] = (
    input_output_angles.polar_angle_conversion_factor * input_output_angles.input_angle_field
)

input_output_angles["retina_location_polar"] = input_output_angles.apply(
    lambda r: octopus_image_to_retina_coordinate(
        r.polar_projected_image_size,
        geometry=patient_geometry,
    ),
    axis=1,
)

input_output_angles["distance_polar"] = euclidean_distance(
    input_output_angles.retina_location,
    input_output_angles.retina_location_polar,
)

## for debugging: plot the complete list of all angles for all of the methods
# input_output_angles

绘制结果#

绘制所有方法的真实(光线追迹)和预测的视网膜位置之间的差异

[15]:
plt.figure()

sns.lineplot(
    input_output_angles,
    x="input_angle_field",
    y="distance_np2",
    label="$2^{\\mathrm{nd}}$ nodal point",
)
sns.lineplot(
    input_output_angles,
    x="input_angle_field",
    y="distance_retina_center",
    label="Retina center",
)
sns.lineplot(input_output_angles, x="input_angle_field", y="distance_pupil", label="Pupil")
sns.lineplot(input_output_angles, x="input_angle_field", y="distance_eyeplan", label="EYEPLAN")
sns.lineplot(input_output_angles, x="input_angle_field", y="distance_corcoran", label="Corcoran")
sns.lineplot(
    input_output_angles,
    x="input_angle_field",
    y="distance_lamberth",
    label="Lamberth projection",
)
sns.lineplot(
    input_output_angles,
    x="input_angle_field",
    y="distance_polar",
    label="Polar projection",
)

plt.grid()
plt.xlabel("Camera angle [°]")
plt.ylabel("Euclidean distance [mm]")
[15]:
Text(0, 0.5, 'Euclidean distance [mm]')
../../_images/examples_%E5%9F%BA%E4%BA%8E%E6%82%A3%E8%80%85%E4%B8%AA%E4%BD%93%E7%89%B9%E5%BE%81%E7%9A%84%E7%9C%BC%E5%BA%95%E7%85%A7%E7%89%87%E4%B8%8E%E4%B8%89%E7%BB%B4%E7%9C%BC%E9%83%A8%E5%BD%B1%E5%83%8F%E9%85%8D%E5%87%86%E7%A0%94%E7%A9%B6_2_analysis_27_1.png
[16]:
column_names = {
    "input_angle_field": ("", "Camera angle [°]"),
    "retina_location": ("", "Retina location"),
    "retina_location_np2": ("2nd nodal point", "Retina location"),
    "distance_np2": ("2nd nodal point", "Difference [mm]"),
    "retina_location_retina_center": ("Retina center", "Difference [mm]"),
    "distance_retina_center": ("Retina center", "Difference [mm]"),
    "retina_location_pupil": ("Pupil", "Retina location"),
    "distance_pupil": ("Pupil", "Difference [mm]"),
    "retina_location_eyeplan": ("EYEPLAN", "Retina location"),
    "distance_eyeplan": ("EYEPLAN", "Difference [mm]"),
    "retina_location_corcoran": ("Corcoran", "Retina location"),
    "distance_corcoran": ("Corcoran", "Difference [mm]"),
    "retina_location_lamberth": ("Lamberth", "Retina location"),
    "distance_lamberth": ("Lamberth", "Difference [mm]"),
    "retina_location_polar": ("Polar", "Retina location"),
    "distance_polar": ("Polar", "Difference [mm]"),
}

table = input_output_angles[column_names.keys()]
table.columns = pd.MultiIndex.from_tuples(column_names.values())
table.map(lambda x: tuple(round(y, 2) for y in x) if isinstance(x, tuple) else x).round(decimals=2)
[16]:
2nd nodal point Retina center Pupil EYEPLAN Corcoran Lamberth Polar
Camera angle [°] Retina location Retina location Difference [mm] Difference [mm] Difference [mm] Retina location Difference [mm] Retina location Difference [mm] Retina location Difference [mm] Retina location Difference [mm] Retina location Difference [mm]
0 0.0 (20.4, 0.0) (20.4, 0) 0.00 (20.4, 0) 0.00 (20.4, 0) 0.00 (20.4, 0) 0.00 (20.4, 0) 0.00 (20.4, 0.0) 0.00 (20.4, 0.01) 0.01
1 10.0 (20.02, 2.9) (20.02, 2.92) 0.02 (20.04, 2.83) -0.07 (20.04, 2.83) -0.07 (19.8, 3.65) 0.79 (20.02, 2.89) -0.00 (20.06, 2.83) -0.07 (20.04, 2.84) -0.06
2 20.0 (18.93, 5.59) (18.92, 5.62) 0.03 (19.0, 5.47) -0.14 (19.0, 5.47) -0.14 (17.96, 7.05) 1.75 (18.93, 5.59) -0.01 (19.03, 5.53) -0.09 (18.98, 5.51) -0.10
3 30.0 (17.23, 7.91) (17.21, 7.92) 0.03 (17.36, 7.77) -0.19 (17.36, 7.77) -0.19 (15.01, 9.74) 2.88 (17.22, 7.91) 0.00 (17.33, 7.98) 0.03 (17.3, 7.83) -0.10
4 40.0 (15.07, 9.7) (15.06, 9.71) 0.02 (15.23, 9.6) -0.20 (15.25, 9.58) -0.22 (11.28, 11.35) 4.13 (15.03, 9.73) 0.05 (14.94, 10.01) 0.35 (15.11, 9.68) -0.04
5 50.0 (12.66, 10.92) (12.65, 10.93) 0.01 (12.77, 10.88) -0.11 (12.85, 10.85) -0.20 (7.26, 11.67) 5.45 (12.51, 10.98) 0.16 (11.87, 11.42) 1.03 (12.54, 10.97) 0.13
6 60.0 (10.2, 11.57) (10.16, 11.57) 0.03 (10.09, 11.58) 0.11 (10.32, 11.55) -0.12 (3.53, 10.76) 6.71 (9.8, 11.62) 0.40 (8.12, 11.9) 2.25 (9.75, 11.63) 0.45
7 70.0 (7.85, 11.7) (7.74, 11.7) 0.11 (7.33, 11.67) 0.52 (7.82, 11.7) 0.03 (0.56, 9.01) 7.77 (7.04, 11.65) 0.81 (3.68, 10.89) 4.40 (6.88, 11.63) 0.97
8 80.0 (5.74, 11.45) (5.49, 11.39) 0.25 (4.61, 11.15) 1.16 (5.48, 11.39) 0.26 (-1.46, 6.93) 8.50 (4.36, 11.07) 1.43 (-1.44, 6.57) 8.81 (4.09, 10.98) 1.71
9 5.0 (20.3, 1.46) (20.3, 1.47) 0.01 (20.31, 1.43) -0.04 (20.31, 1.43) -0.04 (20.25, 1.83) 0.37 (20.3, 1.46) -0.00 (20.31, 1.42) -0.04 (20.31, 1.43) -0.03
10 15.0 (19.56, 4.28) (19.55, 4.31) 0.02 (19.6, 4.18) -0.11 (19.6, 4.19) -0.11 (19.03, 5.41) 1.24 (19.56, 4.28) -0.01 (19.63, 4.21) -0.09 (19.59, 4.21) -0.08
11 25.0 (18.14, 6.81) (18.13, 6.83) 0.03 (18.25, 6.67) -0.17 (18.24, 6.67) -0.17 (16.61, 8.51) 2.29 (18.15, 6.8) -0.01 (18.27, 6.8) -0.05 (18.21, 6.72) -0.11
12 35.0 (16.19, 8.88) (16.18, 8.89) 0.02 (16.35, 8.74) -0.20 (16.35, 8.74) -0.21 (13.22, 10.7) 3.49 (16.18, 8.89) 0.02 (16.22, 9.06) 0.16 (16.26, 8.82) -0.08
13 45.0 (13.89, 10.39) (13.88, 10.39) 0.01 (14.04, 10.31) -0.17 (14.08, 10.29) -0.22 (9.27, 11.67) 4.79 (13.8, 10.43) 0.09 (13.49, 10.81) 0.64 (13.86, 10.4) 0.03
14 55.0 (11.42, 11.31) (11.41, 11.32) 0.02 (11.45, 11.31) -0.02 (11.59, 11.27) -0.17 (5.33, 11.35) 6.10 (11.17, 11.38) 0.26 (10.08, 11.8) 1.55 (11.17, 11.38) 0.27
15 65.0 (9.0, 11.69) (8.94, 11.7) 0.06 (8.71, 11.7) 0.29 (9.05, 11.69) -0.06 (1.93, 9.96) 7.27 (8.42, 11.71) 0.58 (5.98, 11.64) 3.17 (8.32, 11.71) 0.68
16 75.0 (6.76, 11.62) (6.59, 11.59) 0.17 (5.96, 11.49) 0.81 (6.62, 11.6) 0.14 (-0.56, 7.97) 8.18 (5.68, 11.43) 1.10 (1.21, 9.42) 6.12 (5.47, 11.39) 1.31
17 85.0 (4.78, 11.21) (4.45, 11.1) 0.35 (3.31, 10.67) 1.57 (4.41, 11.09) 0.39 (-2.14, 5.92) 8.71 (3.09, 10.57) 1.81 (-4.25, nan) NaN (2.77, 10.42) 2.17