Analyzing Math Items#
Feature extraction across five items, grades 4 and 8.
No assessment items ship with mathipy. Save each item image as <item_id>.png, record the metadata in a CSV with the columns shown below, and point mathipy at that folder.
Setup#
pip install mathipy[all]
export MATHIPY_DATA_DIR=/path/to/items
The metadata CSV is named items.csv and uses these columns:
item_id,grade,year,difficulty,content,image_file
Load the items#
import csv
from mathipy.data import get_sample_csv, get_sample_image, list_sample_images
with open(get_sample_csv()) as f:
reader = csv.DictReader(f)
items = list(reader)
for item in items:
print(f"{item['item_id']} | Grade {item['grade']} | {item['difficulty']} | {item['content']}")
Prints one line per item:
demo-g4-algebra | Grade 4 | Easy | Algebra
demo-g4-number | Grade 4 | Medium | Number Properties and Operations
demo-g4-measure | Grade 4 | Hard | Measurement
demo-g8-geometry | Grade 8 | Easy | Geometry
demo-g8-data | Grade 8 | Easy | Data Analysis, Statistics, and Probability
Available images:
print(list_sample_images())
['demo-g4-algebra.png', 'demo-g4-measure.png', 'demo-g4-number.png',
'demo-g8-data.png', 'demo-g8-geometry.png']
Text features#
Use MultimodalOCR to extract text and math expressions from item images. This requires a Gemini or OpenAI API key.
from mathipy import MultimodalOCR
ocr = MultimodalOCR(provider="gemini")
image_path = str(get_sample_image("demo-g4-algebra"))
result = ocr.extract(image_path)
print(result["full_text"])
print(result["math_expressions"])
print(result["answer_choices"])
full_text holds the transcription, math_expressions the detected expressions, and answer_choices a letter-to-option mapping.
Readability#
LaTeX and math symbols are replaced with placeholders so they don’t inflate complexity scores.
from mathipy import ReadabilityAnalyzer
analyzer = ReadabilityAnalyzer()
texts = {
"demo-g4-algebra": "Maya is Y years old. Maya cousin is 4 years older than Maya. "
"Which expression represents Maya cousin age in years? "
"A) Y+4 B) Y-4 C) Y*4 D) Y/4",
"demo-g4-number": "Multiply. 315 * 6 =",
"demo-g8-geometry": "The six points shown on the diagram represent parks. "
"The distances, in meters, along the paths between parks are given. "
"Rosa wants to travel from her park to Cedar park in the shortest distance. "
"On which paths should Rosa travel? "
"Select the appropriate paths to show your answer.",
}
for item_id, text in texts.items():
result = analyzer.analyze(text)
print(f"{item_id}: FK Grade={result['flesch_kincaid_grade']:.1f}, "
f"Reading Ease={result['flesch_reading_ease']:.1f}")
The strings above are made up, not copied from real items. Replace them with OCR output from an item folder.
Values depend on whether textstat is installed; mathipy[nlp] supplies it, and a built-in fallback is used otherwise.
Math content#
Classify items by CCSSM math domain and extract math features.
from mathipy import MathContentAnalyzer
math_analyzer = MathContentAnalyzer()
for item_id, text in texts.items():
result = math_analyzer.analyze(text)
domain = result["domain_classification"]
print(f"{item_id}: domain={domain['primary']} "
f"(confidence={domain['confidence']:.2f}), "
f"density={result['math_density']:.2f}, "
f"terms={result['vocabulary']['math_terms']}")
demo-g4-algebra: domain=algebra (confidence=1.00), density=0.31, terms=['expression']
demo-g4-number: domain=arithmetic (confidence=1.00), density=0.20, terms=['multiply']
demo-g8-geometry: domain=arithmetic (confidence=0.00), density=0.00, terms=[]
The geometry item scores zero because its content sits in a diagram, not the text — text-only features miss math that lives in a picture. The domain vocabulary has no measurement category, so measurement items cannot be classified.
Cognitive load#
Element counts, operation counts, and density ratios.
from mathipy import CognitiveLoadEstimator
estimator = CognitiveLoadEstimator()
for item_id, text in texts.items():
result = estimator.estimate(text)
print(f"{item_id}: elements={result['numeric_elements']}, "
f"ops={result['operation_count']}, "
f"vars={result['variable_count']}, "
f"density={result['element_density']:.3f}")
demo-g4-algebra: elements=5, ops=4, vars=5, density=0.34
demo-g4-number: elements=2, ops=2, vars=0, density=0.40
demo-g8-geometry: elements=0, ops=0, vars=0, density=0.00
The geometry item has no numbers at all; its load sits in the long instructions, which word and sentence counts pick up instead.
Visual features#
Image complexity features from the item screenshots.
from mathipy import VisualFeatureExtractor
extractor = VisualFeatureExtractor()
for item in items[:3]:
image_path = str(get_sample_image(item["item_id"]))
features = extractor.extract(image_path)
dims = features["dimensions"]
score = features["complexity_score"]
shapes = features["structural_elements"]
print(f"{item['item_id']}: "
f"{dims['width']}x{dims['height']}, "
f"edge_ratio={score['edge_ratio']:.3f}, "
f"shapes={score['total_shapes']}, "
f"lines={shapes['line_count']}")
demo-g4-algebra: 520x390, edge_ratio=0.105, shapes=1, lines=39
demo-g4-number: 872x147, edge_ratio=0.072, shapes=1, lines=0
demo-g4-measure: 640x480, edge_ratio=0.118, shapes=4, lines=22
One row per item#
The pipeline produces one feature vector per item:
import csv
from mathipy import VisualFeatureExtractor
from mathipy.data import get_sample_csv, get_sample_image
extractor = VisualFeatureExtractor()
with open(get_sample_csv()) as f:
items = list(csv.DictReader(f))
results = []
for item in items:
image_path = str(get_sample_image(item["item_id"]))
visual = extractor.extract(image_path)
results.append({
"item_id": item["item_id"],
"grade": item["grade"],
"difficulty": item["difficulty"],
"content": item["content"],
"edge_ratio": visual["complexity_score"]["edge_ratio"],
"total_shapes": visual["complexity_score"]["total_shapes"],
"lines": visual["structural_elements"]["line_count"],
})
for r in results:
print(f"{r['item_id']:16s} | {r['difficulty']:6s} | "
f"edge_ratio={r['edge_ratio']:.3f} | "
f"shapes={r['total_shapes']:3d} | lines={r['lines']:3d}")
Text features are added after OCR extraction to complete the table.
Representation type#
The features above measure visual complexity, not content. Classification assigns the representation type and instructional role.
from mathipy import VisualModelClassifier, flags_by_group
from mathipy.visual import visual_models
classifier = VisualModelClassifier(provider="gemini")
result = classifier.classify(str(get_sample_image("demo-g8-data")))
types = [m for m in visual_models if result[m]]
print(f"primary : {result['primary']}")
print(f"function: {result['function']}")
print(f"types : {types}")
print(f"families: {[k for k, v in flags_by_group(result).items() if v]}")
primary : circle_graph
function: essential
types : ['circle_graph', 'table']
families: ['visual_group_data_display', 'visual_group_organizational']
Two cautions. The same image can come back with a different label on a second run. The labels have not been checked against human coders, so hand-code a sample and compare before using them.