The Audiogram as a Python Object - utilities for creating python representations of clinical audiometric data
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file audiogram-0.1.0.tar.gz.
File metadata
- Download URL: audiogram-0.1.0.tar.gz
- Upload date:
- Size: 39.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32c072c5965566f23a030ce364908e3a31aed1b362e2661b250aa62c5b4a9735
|
|
| MD5 |
69ed9bf73e646a4fa217c6d586fee000
|
|
| BLAKE2b-256 |
132b4a341871b633b2c053d47220c7ecfdc9e866433dd1f5d25f1074a845fa3c
|
File details
Details for the file audiogram-0.1.0-py3-none-any.whl.
File metadata
- Download URL: audiogram-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75d29f47f98dd1d69cdcbcf0321c77e74fffdff359d5286b8f7ece0193cb0b63
|
|
| MD5 |
220e19f4c6f713043c31c66de2c5565d
|
|
| BLAKE2b-256 |
d756e2ad3f8aa2d85ac89f1fdbc7237b9add3e58dd5c877732994ad38685d344
|