API Reference#
Readability#
ReadabilityAnalyzer#
Class for analyzing text readability with math-aware normalization.
from mathipy import ReadabilityAnalyzer
analyzer = ReadabilityAnalyzer()
result = analyzer.analyze("Solve for x: 2x + 5 = 15")
Returns a dictionary with:
- flesch_reading_ease
Flesch Reading Ease score (0–100)
- flesch_kincaid_grade
Flesch-Kincaid grade level
- gunning_fog
Gunning Fog index
- smog_index
SMOG index
- automated_readability_index
ARI score
- coleman_liau_index
Coleman-Liau index
- linsear_write_formula
Linsear Write formula
- dale_chall_readability
Dale-Chall readability score
- average_grade_level
Average of FK, Fog, and SMOG
- low_confidence
Trueif text is shorter than 20 words
Math Content#
MathContentAnalyzer#
Class for math content analysis and CCSSM domain classification.
from mathipy import MathContentAnalyzer
analyzer = MathContentAnalyzer()
result = analyzer.analyze("What is the area of a triangle with base 6 and height 4?")
Returns a dictionary with:
- pattern_matches
Detected math patterns (equations, fractions, etc.)
- symbol_counts
Counts of math symbols by type
- total_math_symbols
Total math symbols found
- numbers
Extracted numbers with count, range, and properties
- vocabulary
Matched math terms and counts
- domain_classification
Primary domain, confidence, and scores
- math_density
Ratio of math patterns to word count
Domain categories: arithmetic, algebra, geometry, statistics, calculus, fractions
Cognitive Load#
CognitiveLoadEstimator#
Class for estimating cognitive load components.
from mathipy import CognitiveLoadEstimator
estimator = CognitiveLoadEstimator()
result = estimator.estimate(text, readability_grade=5.2, math_terms=["equation", "solve"])
Returns a dictionary with:
- numeric_elements
Count of numbers in text
- variable_count
Count of single-letter variables
- operation_count
Count of math operations (+, -, *, /, ^, =, <, >)
- word_count
Total words in text
- sentence_count
Total sentences in text
- math_term_count
Count of CCSSM-aligned math keywords
- element_density
Ratio of (numeric elements + variables) to word count
- avg_sentence_length
Average words per sentence
Visual#
VisualFeatureExtractor#
Class for extracting complexity features from assessment images.
Requires: pip install mathipy[vision]
from mathipy import VisualFeatureExtractor
extractor = VisualFeatureExtractor()
features = extractor.extract("item_image.png")
Accepts a file path, Path object, or numpy array. Returns a dictionary with:
- dimensions
Width, height, aspect ratio, channels
- pixel_statistics
Mean, std, min, max, median, contrast
- edge_metrics
Canny edge ratio, Sobel/Laplacian statistics
- structural_elements
Detected lines, circles, shapes (triangles, rectangles, etc.)
- frequency_domain
Low/mid/high frequency energy ratios
- complexity_score
Summary with edge_ratio, total_shapes, and high_freq_ratio
OCR#
MultimodalOCR#
Class for extracting text and math from images using vision LLMs.
Requires: pip install mathipy[ocr] and a GEMINI_API_KEY or OPENAI_API_KEY.
from mathipy import MultimodalOCR
ocr = MultimodalOCR(provider="gemini")
result = ocr.extract("item_image.png")
Parameters:
- provider
"gemini"or"openai"- model
Model name (defaults to
gemini-2.5-flashorgpt-4o)- api_key
API key (or set via
.envfile)
Accepts image path, URL, bytes, PDF, DOCX, or text file. Returns a dictionary with:
- content_type
text_only,image_only, ormixed- full_text
All extracted text
- image_description
Visual content description
- question_text
Main question/problem statement
- math_expressions
List of LaTeX expressions
- answer_choices
Dictionary of answer choices
- extraction_confidence
Confidence score (0–1)
Visual Model Classification#
VisualModelClassifier#
Classify which visual model types appear in an assessment image.
Requires: pip install mathipy[ocr] and a GEMINI_API_KEY or OPENAI_API_KEY.
from mathipy import VisualModelClassifier
classifier = VisualModelClassifier(provider="gemini")
result = classifier.classify("item_image.png")
result = classifier.classify("item_image.png", votes=3)
result = classifier.classify("item_image.png", item_text="A store sells 3 apples for $2...")
Parameters:
- provider
"gemini"or"openai"- model
Model name (defaults to
gemini-2.5-flashorgpt-4o)- api_key
API key (or set via
.envfile)
Returns a dictionary with a boolean per model type, "primary" (str), "function" (str: essential, representational, decorative, or unknown), and "model_count" (int). The votes argument runs multiple independent calls and merges flags by majority and primary/function by mode.
The item_text argument supplies the item’s text so the function judgment can compare the image against it. Without item_text, the essential vs representational distinction is reliable only when the image contains the complete item.
visual_model_definitions#
Dictionary with a one-line decision rule for each type. The same rules appear in the classifier prompt, so human coders and the model work from a single codebook.
from mathipy import visual_model_definitions
print(visual_model_definitions["box_plot"])
visual_model_groups#
Dictionary mapping each visual model type to a broader category.
from mathipy import visual_model_groups
print(visual_model_groups["number_line"]) # "number_quantity"
visual_model_info#
List of tuples with (model_name, group, CCSSM_domains, grade_band) for each visual model type.
from mathipy import visual_model_info
for name, group, domains, grades in visual_model_info[:3]:
print(f"{name}: {group}, {domains}, {grades}")
flags_by_group#
Collapses the per-type boolean flags into one visual_group_* key per representation family. Several types are rare in any single corpus, so the families give a workable predictor set for modeling while the per-type flags remain available for description.
from mathipy import flags_by_group, group_names
print(group_names)
# ['data_display', 'geometric', 'number_quantity', 'organizational',
# 'other_visual', 'part_whole_model']
print(flags_by_group(result))
# {'visual_group_data_display': 1, 'visual_group_geometric': 0, ...}
Keys other than the type names are ignored, so a full classification result can be passed directly.
Multimodal Item#
MultimodalAnalyzer#
Runs every analyzer over one item and returns a flat prefix_* dict. Aliased as
ItemFeatureExtractor.
from mathipy import MultimodalAnalyzer
row = MultimodalAnalyzer().extract("Which fraction is larger, 2/3 or 3/4?")
Notation#
normalize_math_notation#
Converts LaTeX and hybrid-ASCII math markup to one plain-text grammar, so the same expression is not counted as several different things.
Morphology#
morphology_features#
Counts of meaning-bearing morphemes below the word: nominalization, Greek and Latin roots, derivational suffixes.
Symbolic Notation#
symbolic_features#
Counts of relations carried by notation rather than by wording. channel_pairs
names the sym_* measures that have a rel_* counterpart, so the two channels
can be compared.
Fractions#
fraction_features#
Structural features of every fraction in the text, in digit or word form: denominator magnitude, unit fractions, unlike denominators, unreduced forms.
Cross-modal Reference#
crossmodal_features#
Every cross-modal measure for one item. deictic_features counts pointers to
the image (“shown above”); label_features counts references to a labeled
element inside it (“angle A”).
Geometry#
classify_shapes#
Counts of geometric shape subtypes from contour analysis: triangle and quadrilateral subtypes, regular polygons, fill and partition structure.
Feature Redundancy#
dependent_sets#
Every composite in a column set whose components are also present, so a caller
can see what would make a correlation matrix singular. drop_composites and
drop_components apply the two reductions; composite_features and
near_duplicates declare the structure.
Figure Region#
crop_to_box#
Crops an image to a figure box and returns the region measured, "figure" or
"frame". trim_margin removes capture margin so a ratio does not depend on
how much white surrounds the item.
flags_by_group, flags_by_sign#
Collapse the per-type flags into representation families (group_names) or
Peircean sign classes (sign_names, visual_model_signs). visual_functions
lists the three instructional function labels.
Document Segmentation#
Released test documents hold many items in one file, marked by an identifier line. Whether that line precedes or follows the block it names varies by publisher, and both readings parse without error — so a wrong choice is silent and shifts every item’s content by one position.
from mathipy import segment_docx, check_alignment
items = segment_docx(
"released-test.docx",
marker="Question ID:",
label_position="trailing",
skip_prefixes=["_____", "Item Detail"],
section_markers={"Item Detail - Question Description": "description"},
)
label_position is "leading" when the marker introduces the block below it, "trailing" when it names the block above. section_markers routes text under a named header into its own field, so descriptive metadata does not contaminate the item body. Each record holds item_id, text, images, and any named sections.
Confirm the reading before an analysis depends on it:
check_alignment(items, reference={item_id: expected_description})
# {'own': 1180, 'next': 84, 'previous': 6, 'compared': 1270,
# 'proportions': {...}, 'verdict': 'aligned'}
A verdict of shifted_next means identifiers lag the content by one position — switch label_position and re-check. Comparing image counts between the two readings is a second signal: the wrong reading silently drops content that precedes the first marker.
Cohesion#
Discourse measures for extended writing and dialogue, where readability formulas do not apply. Deterministic, standard library only, no network call.
from mathipy import cohesion_features
cohesion_features("The slope rises. However it falls later. Therefore we checked.")
# {'connective_additive': 0, 'connective_causal': 1, 'connective_adversative': 1,
# 'connective_total': 2, 'connective_per_100w': 16.7,
# 'overlap_adjacent_mean': 0.25, 'lexical_diversity': 0.85,
# 'pronoun_per_100w': 8.3, 'unit_count': 3}
Accepts a string, split into sentences, or a sequence of units such as turns from segment_turns.
Individual measures are available as connective_density, lexical_overlap, lexical_diversity, and pronoun_density. Connectives are grouped as additive, causal, temporal, adversative, and clarifying, matched longest-phrase-first so on the other hand is not double-counted as but. Lexical diversity uses a moving-average ratio so it does not fall with text length. Numerals are excluded from token counts, since numeric values are not lexical repetition.
De-identification#
Classroom transcripts and student writing carry names and contact details. Run this before anything else touches the data. Local and deterministic — nothing is transmitted.
from mathipy import deidentify_turns, scan, segment_turns
turns = segment_turns("lesson.vtt", transcript_format="vtt")
clean = deidentify_turns(turns)
# speaker labels become "Speaker 1", "Speaker 2"; names in text become [NAME]
scan(text) # report what is present without changing anything
Masks emails, phone numbers, URLs, social security numbers, student IDs, dates, and titled names such as Ms. Rivera. deidentify_turns also removes speaker names from the body text, since speakers address each other by name. Pass names=[...] to remove additional names, and keep=["url"] to retain a category.
Matching is pattern-based, so it will not catch every name. Review scan output before treating a transcript as clean.
Dialogue#
Splits a transcript into speaker turns and reports turn-level measures. Runs locally with no network call, so transcripts are never transmitted.
from mathipy import segment_turns, turn_measures, check_speakers
turns = segment_turns("lesson.vtt", transcript_format="vtt")
turn_measures(turns)
# {'turns': 42, 'speakers': 3, 'talk_share': {'Teacher': 0.61, ...},
# 'mean_turn_words': 14.3, 'uptake_mean': 0.18}
Formats: plain for Speaker: text lines, vtt and srt for captions, csv with speaker_key and text_key. uptake_mean is the lexical overlap between adjacent turns by different speakers.
Pass text= to segment an in-memory string, with separator for turns that are not newline-delimited and strip_pattern to drop inline annotations:
turns = segment_turns(
text=row["conversation"],
separator="|EOM|",
strip_pattern=r"\((?:generic|focus|probing|telling)\)",
)
check_speakers flags labels likely to be transcription artifacts — speakers appearing once, near-duplicate spellings, and runs of consecutive same-speaker turns.
Readability formulas assume written prose with sentence boundaries and are not valid on transcripts. Use the math-content and dialogue measures instead.
Validation#
Automated labels need checking against human coders. This module draws the sample, emits the coding instrument, and scores agreement. Standard library only.
from mathipy import (
stratified_sample, write_coding_sheets, write_label_studio,
score_agreement, disagreements, ocr_rubric, visual_rubric,
)
sample = stratified_sample(records, ["tier", "band"], n=120, seed=2026)
write_coding_sheets(sample, ocr_rubric, "output/validation", carry=["item_id", "image", "text"])
write_label_studio(sample, visual_rubric, "output/validation")
stratified_sample allocates proportionally with a floor per stratum, so rare categories are not lost. write_coding_sheets writes one blank sheet per rater plus the rubric. write_label_studio writes a task file and a labeling-interface config for Label Studio, carrying provenance fields into each task.
Score the completed sheets:
score_agreement(rows_a, rows_b, list(ocr_rubric))
# {'hallucinated_content': {'agreement': 0.96, 'kappa': 0.71, 'n': 135}, ...}
disagreements(rows_a, rows_b, list(ocr_rubric)) # rows needing adjudication
ocr_rubric covers extraction completeness and hallucinated content. visual_rubric covers the types and the function labels, drawn from visual_model_definitions so coders and the classifier share one definition.
Data Handling#
MultimodalOCR.extract() and VisualModelClassifier.classify() send image data to the configured provider. Every other analyzer runs locally.
Defaults are https://generativelanguage.googleapis.com/v1beta and https://api.openai.com/v1. Setting base_url redirects to any OpenAI-compatible endpoint, including a local server, in which case nothing is transmitted. Results record provider, model, base_url, and extracted_at.
Provider terms differ by tier and change. As of this release, unpaid Gemini API content may be used to improve Google products and read by human reviewers; paid-tier content is not used for training; EEA, Swiss, and UK users get paid terms on all tiers. Verify at Gemini and OpenAI.
Before sending secure or restricted items: check the governing agreement, confirm which tier the key belongs to, and consider a local endpoint.
The package sends no telemetry, writes no extracted content to disk, downloads no models, and distributes no assessment items.
compute_interrater_reliability#
Agreement and Cohen’s kappa between two raters, over matched rows.
safe_get#
Retrieves a nested value from a dict, returning a default on any miss.
Provenance and Limitations#
Representation types follow Lesh et al. (1987) and Duval (2006); the data-display family follows Friel et al. (2001); domain and grade-band columns map to CCSSM (NGA & CCSSO, 2010). The pattern_visual, picture, and other rows are author-derived.
The three function labels adapt Levin et al. (1987), retaining decorative and representational and collapsing interpretational and transformational into essential, because assessment items call for a solve-relevance judgment. This is an adaptation, not an implementation of the five-category scheme.
Classification is not deterministic: temperature is 0.1, no seed is available, and hosted models change over time. Record the model identifier and date, cache results rather than re-classifying, and report vote agreement when votes > 1.
Classifier accuracy against human coders has not been established. Treat labels as provisional and validate against hand-coded items before relying on them for inference.
References#
Carney, R. N., & Levin, J. R. (2002). Pictorial illustrations still improve students’ learning from text. Educational Psychology Review, 14(1), 5-26. https://doi.org/10.1023/A:1013176309260
Duval, R. (2006). A cognitive analysis of problems of comprehension in a learning of mathematics. Educational Studies in Mathematics, 61(1-2), 103-131. https://doi.org/10.1007/s10649-006-0400-z
Friel, S. N., Curcio, F. R., & Bright, G. W. (2001). Making sense of graphs. Journal for Research in Mathematics Education, 32(2), 124-158.
Lesh, R., Post, T., & Behr, M. (1987). Representations and translations among representations in mathematics learning and problem solving. In C. Janvier (Ed.), Problems of representation in the teaching and learning of mathematics (pp. 33-40). Erlbaum.
Levin, J. R., Anglin, G. J., & Carney, R. N. (1987). On empirically validating functions of pictures in prose. In D. M. Willows & H. A. Houghton (Eds.), The psychology of illustration (Vol. 1, pp. 51-85). Springer.
National Governors Association Center for Best Practices & Council of Chief State School Officers. (2010). Common Core State Standards for Mathematics.
Sample Data#
No assessment items are distributed with mathipy. These helpers resolve paths inside a directory supplied by the user — set MATHIPY_DATA_DIR to a folder holding item images and a metadata CSV.
export MATHIPY_DATA_DIR=/path/to/items
from mathipy.data import data_directory, get_sample_csv, get_sample_image, list_sample_images
print(data_directory())
images = list_sample_images() # [] when the directory holds no PNGs
csv_path = get_sample_csv() # path only; may not exist
image_path = get_sample_image("demo-g4-algebra")
The functions return paths without requiring the files to exist, so check Path.exists() before reading. get_sample_image rejects item IDs containing path separators or characters outside A-Z a-z 0-9 _ - space #.
Mathematics Register#
Existing item features count how much text an item carries. These describe what the language does: which relation the wording encodes, which kind of number it names, and whether an everyday word is being used in its mathematical sense.
Depends only on the standard library.
register_features#
from mathipy import register_features
register_features("Ann has 5 less than Ben. The table shows 1/2.")
# {'rel_comparison': 1, 'rel_order_reversed': 1, 'rel_partitive': 1, ...
# 'num_fraction': 1, 'homonym_count': 1, 'homonym_unique': 1}
Returns every measure below in one dictionary.
relational_features#
Counts of the relations the wording encodes: comparison, rate, multiplicative, partitive, distribution, and division.
rel_order_reversed counts the marked comparative, in which a quantity precedes
the comparative as in five less than Ben. The operands then appear in the
reverse of the order the operation needs, which is a documented source of error
(Lewis & Mayer, 1987).
from mathipy import relational_features
relational_features("Ann has 5 less than Ben.")
# {'rel_comparison': 1, 'rel_order_reversed': 1, 'rel_total': 1, ...}
number_features#
Numbers split by what they do: count a set, index a position, name a part, or
identify an object. Treating every number as a count misdescribes ordinary text
(Woodin & Winter, 2024), and the kinds make different demands on a reader.
num_round counts values that signal approximation.
from mathipy import number_features
number_features("The 3rd bus carries 40 people and 1/2 of the seats are free.")
# {'num_cardinal': 1, 'num_ordinal': 1, 'num_fraction': 1, 'num_round': 1, ...}
homonym_features#
Everyday words carrying a distinct mathematical sense, such as table, mean, product, power, base, root, plane, right, and odd. A reader must choose between the two senses, which is one of the demands the register places on comprehension (Schleppegrell, 2007).
from mathipy import homonym_features
homonym_features("The table shows the mean and the range.")
# {'homonym_count': 3, 'homonym_unique': 3}