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.