Skip to main content

audiogram

A Python library for representing clinical audiometric data as typed objects.

Audiograms are structured data — frequencies, thresholds, air and bone conduction, masking, no-response flags — but they're routinely stored as flat dictionaries, wide-format CSVs, or strings in EMR free-text fields. audiogram gives them a proper object model with metrics, asymmetry analysis, serialization, and optional matplotlib plotting.

Install

pip install audiogram

With matplotlib plotting support:

pip install audiogram[plot]

Requires Python 3.11+. No required dependencies for the core library.

Quick start

from audiogram import ThresholdPoint, EarAudiogram, BinauralAudiogram

left = EarAudiogram(
    air={
        500:  ThresholdPoint(25.0),
        1000: ThresholdPoint(30.0),
        2000: ThresholdPoint(35.0),
        4000: ThresholdPoint(50.0),
    },
    bone={
        1000: ThresholdPoint(20.0, masked=True),
        2000: ThresholdPoint(25.0, masked=True),
    },
)

right = EarAudiogram(
    air={
        500:  ThresholdPoint(20.0),
        1000: ThresholdPoint(25.0),
        2000: ThresholdPoint(40.0),
        4000: ThresholdPoint(120.0, nr=True),  # no response — store max output + nr=True
    },
)

ba = BinauralAudiogram(
    left=left,
    right=right,
    audiogram_id="aud-001",
    subject_id="pt-042",
    performed_at="2024-06-15",
    source="clinic",
)

Key concepts

ThresholdPoint

The atomic unit — one threshold measurement at one frequency.

ThresholdPoint(threshold_db=30.0)                    # unmasked, response present
ThresholdPoint(30.0, masked=True)                    # contralateral ear was masked
ThresholdPoint(120.0, nr=True)                       # no response; value = max output tested

Plain floats are accepted anywhere a ThresholdPoint is expected and are coerced automatically.

WordRecognitionScore

Pairs a percent-correct score with its presentation level.

WordRecognitionScore(92.0, 70.0)                     # 92% at 70 dB HL
WordRecognitionScore(88.0, 90.0, masked=True)         # masked
WordRecognitionScore(92.0, 70.0, word_list="CNC")     # with word list

EarAudiogram

One ear's air and bone conduction thresholds, plus optional speech audiometry.

ear = EarAudiogram(
    air={500: 20.0, 1000: 25.0, 2000: 30.0},
    srt=25.0,
    wrs=[WordRecognitionScore(92.0, 70.0, word_list="CNC")],
)

ear.pta()                                            # 25.0 — 3-tone PTA (default)
ear.pta(standard="4tone")                            # 4-tone PTA (500/1000/2000/4000)
ear.pta(standard="aao_hns")                          # AAO-HNS (500/1000/2000/3000 w/ fallback)
ear.pta(freqs=(500, 1000, 2000, 4000))              # explicit freq override
ear.pta(pathway="bone")                              # bone conduction PTA
ear.pta(require_all=True)                            # None if any freq is missing
ear.available_frequencies()                          # [500, 1000, 2000]
ear.available_frequencies("bone")                    # bone frequencies

# Speech audiometry
ear.srt                                              # 25.0
ear.best_wrs()                                       # WordRecognitionScore(92.0, 70.0, ...)
ear.srt_pta_agreement()                              # 0.0 (SRT - PTA; should be within ±10 dB)

BinauralAudiogram

Both ears with test-level metadata.

ba.get_threshold(1000, "left")                       # ThresholdPoint
ba.get_threshold(1000, "left", pathway="bone")

ba.pta()                                             # {"left": 30.0, "right": 28.3}
ba.pta(standard="aao_hns")                           # AAO-HNS PTA for each ear
ba.better_ear_pta()                                  # 28.3
ba.worse_ear_pta()                                   # 30.0
ba.symmetry()                                        # {500: 5.0, 1000: 5.0, ...} (left - right)

Air-bone gap & loss type

# Per-frequency air-bone gap
left.abg()                                           # {500: 5.0, 1000: 10.0, ...}
left.abg(mask_warning=True)                          # warns if any bone thresholds are unmasked

# ABG PTA — average of the gaps
left.abg_pta()                                       # WHO 2021 default: 500/1000/2000/4000
left.abg_pta(standard="aao_hns")                     # AAO-HNS: 500/1000/2000/3000 (with fallback)

# Loss type classification
left.loss_type()                                     # "sensorineural", "conductive", "mixed", or "normal"
left.loss_type(standard="aao_hns")

Classification: air PTA ≤ 25 → normal; ABG-PTA < 10 → sensorineural; ABG-PTA ≥ 10 with normal bone → conductive; ABG-PTA ≥ 10 with elevated bone → mixed. Returns None when air or bone data is insufficient to classify.

Severity

WHO 2021 hearing loss grading based on pure-tone average. Also supports AAO-HNS (500/1000/2000/3000 Hz with avg(2000, 4000) fallback when 3000 is absent).

left.severity()                                      # "moderate" (WHO 2021, 4-tone PTA)
left.severity(standard="aao_hns")                    # AAO-HNS method
left.severity(freqs=(500, 1000, 2000))               # explicit freq override
ba.severity()                                        # {"left": "moderate", "right": "mild"}

Grades: normal (≤25), mild (26–40), moderate (41–55), moderately_severe (56–70), severe (71–90), profound (>90).

Asymmetry

from audiogram import ASYMMETRY_CRITERIA

# Named built-in criteria
ba.is_asymmetric("any_15db")             # any frequency >= 15 dB interaural difference
ba.is_asymmetric("any_30db")             # any frequency >= 30 dB
ba.is_asymmetric("two_consecutive_10db") # two consecutive freqs >= 10 dB apart
ba.is_asymmetric("two_consecutive_15db") # two consecutive freqs >= 15 dB apart
ba.is_asymmetric("three_consecutive_10db") # three consecutive freqs >= 10 dB apart
ba.is_asymmetric("pta_15db")             # PTA difference between ears >= 15 dB
ba.is_asymmetric("3k_rule")              # >= 15 dB at 3000 Hz
ba.is_asymmetric("wrs_15pct")            # best WRS differs by > 15%
ba.is_asymmetric("nr_one_side")          # one ear NR, other has a threshold

# Custom criterion — any callable (BinauralAudiogram) -> bool | None
ba.is_asymmetric(lambda ba: abs(ba.pta()["left"] - ba.pta()["right"]) > 20)

# Interaural differences per frequency
for d in ba.interaural_differences():
    print(d.freq_hz, d.difference_db, d.better_ear, d.nr_involved)

is_asymmetric returns True, False, or None (indeterminate — e.g. missing frequencies). Clinical criteria in the built-ins are reasonable defaults; verify against your target guideline before using for clinical decision support.

Serialization

Dict/JSON is the canonical lossless interchange format — it round-trips all data including thresholds, bone conduction, masking, NR, and speech audiometry.

# Dict / JSON — full fidelity
ba2 = BinauralAudiogram.from_dict(ba.to_dict())
ba2 = BinauralAudiogram.from_json(ba.to_json())

# Long-form threshold rows — one dict per threshold observation
rows = ba.to_long_rows()
ba2  = BinauralAudiogram.from_long_rows(rows)

# Long-form speech rows — one dict per speech measurement
speech_rows = ba.to_speech_rows()

# Combined long-form — thresholds + speech with measure_type discriminator
all_rows = ba.to_long_rows(include_speech=True)
ba2 = BinauralAudiogram.from_long_rows(all_rows)

# With pandas or polars
import polars as pl
df = pl.DataFrame(ba.to_long_rows())

# Normalized two-table output (mirrors a relational DB schema)
test_row, obs_rows = ba.to_table_rows()
Format Thresholds Speech Lossless
Dict / JSON yes yes yes
Wide row yes yes yes
Long rows (default) yes no thresholds only
Long rows (include_speech=True) yes yes yes
Speech rows no yes speech only

Wide-format ingest

Research datasets and EMR exports typically store audiograms in wide format — one row per audiogram, columns like R_AC_500, L_BC_1K, etc. Every dataset uses different column names.

audiogram defines a canonical wide column convention and a simple column-mapping workflow so you can ingest any wide dataset with a one-time mapping dict.

Canonical wide column convention

{ear}_{pathway}_{frequency}            # threshold value
{ear}_{pathway}_{frequency}_masked     # boolean (default False if absent)
{ear}_{pathway}_{frequency}_nr         # boolean (default False if absent)

Examples: r_air_500, l_bone_1000, r_air_4000_masked, l_air_250_nr

Ears: r / l. Pathways: air / bone. Frequencies: raw integers (Hz).

Import from wide format

# Your dataset has columns like R_AC_500, L_AC_1K, etc.
# Define a column map once:
column_map = {
    "PatientID":  "subject_id",
    "TestDate":   "performed_at",
    "R_AC_500":   "r_air_500",
    "R_AC_1K":    "r_air_1000",
    "R_AC_2K":    "r_air_2000",
    "L_AC_500":   "l_air_500",
    "L_AC_1K":    "l_air_1000",
    "L_AC_2K":    "l_air_2000",
}

# Ingest a single row
ba = BinauralAudiogram.from_wide_row(row, column_map=column_map)

# Ingest a whole DataFrame
audiograms = [BinauralAudiogram.from_wide_row(row, column_map=column_map)
              for row in df.to_dict(orient="records")]

Missing or empty values are silently skipped. Masked and NR flags default to False when absent — which is the common case for research datasets that only contain air thresholds.

Export to wide format

wide_row = ba.to_wide_row()
# {'audiogram_id': 'aud-001', 'r_air_500': 20.0, 'r_air_1000': 25.0, ...}

Helper functions

from audiogram import wide_column_name, parse_wide_column, canonical_wide_columns

wide_column_name("right", "air", 500)           # "r_air_500"
parse_wide_column("l_bone_1000_masked")          # {"ear": "left", "pathway": "bone", ...}
canonical_wide_columns([500, 1000, 2000])        # all column names for those frequencies

Summary metrics

summary() returns a flat dict of all computed metrics — ready to merge into a DataFrame row.

ba.summary()
# {
#     'pta_right': 35.0, 'pta_left': 28.3,
#     'better_ear_pta': 28.3, 'worse_ear_pta': 35.0,
#     'right_severity': 'mild', 'left_severity': 'mild',
#     'right_abg_pta': 15.0, 'left_abg_pta': 5.0,
#     'right_loss_type': 'conductive', 'left_loss_type': 'sensorineural',
#     'asymmetric_any_15db': False, ...
# }

# Pass standard through to severity, ABG, and loss type
ba.summary(standard="aao_hns")

# Filter by category
ba.summary(include=["pta", "abg"])
ba.summary(exclude=["frequency_count"])

Enrichment workflow

# Ingest a wide dataset and enrich with computed metrics
for i, row in df.iterrows():
    ba = BinauralAudiogram.from_wide_row(row, column_map=col_map)
    for k, v in ba.summary().items():
        df.loc[i, k] = v

Custom metrics

Register your own metric functions — they automatically appear in summary() output.

from audiogram import register_summary_metric

def hearing_loss_degree(ba):
    ptas = ba.pta()
    result = {}
    for ear in ("right", "left"):
        pta = ptas[ear]
        if pta is None:
            result[f"{ear}_hl_degree"] = None
        elif pta <= 25:
            result[f"{ear}_hl_degree"] = "normal"
        elif pta <= 40:
            result[f"{ear}_hl_degree"] = "mild"
        # ... etc
    return result

register_summary_metric("hl_degree", hearing_loss_degree)
ba.summary()  # now includes right_hl_degree, left_hl_degree

Plotting

Requires pip install audiogram[plot].

# Both ears — clinic-style two-panel layout
ba.plot()

# Single ear with bone conduction overlay
ba.left.plot(ear="left", show_bone=True)

Symbols follow ASHA convention: O (right air), X (left air), [ / ] (bone). Connecting lines join response-present air conduction points (including masked). NR points are plotted but not connected.

License

MIT

Metadata

Release files for audiogram 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for audiogram 0.1.0
File Size Uploaded
audiogram-0.1.0.tar.gz 39.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for audiogram 0.1.0
File Interpreter ABI Platform
audiogram-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 79.6 kB

Release files / audiogram-0.1.0.tar.gz

Download URL audiogram-0.1.0.tar.gz
Size 39.1 kB
Tags Source
SHA-256 checksum
How to use checksums
32c072c5965566f23a030ce364908e3a31aed1b362e2661b250aa62c5b4a9735
BLAKE2b-256 checksum
How to use checksums
132b4a341871b633b2c053d47220c7ecfdc9e866433dd1f5d25f1074a845fa3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.8

Release files / audiogram-0.1.0-py3-none-any.whl

Download URL audiogram-0.1.0-py3-none-any.whl
Size 40.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
75d29f47f98dd1d69cdcbcf0321c77e74fffdff359d5286b8f7ece0193cb0b63
BLAKE2b-256 checksum
How to use checksums
d756e2ad3f8aa2d85ac89f1fdbc7237b9add3e58dd5c877732994ad38685d344
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.8

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page