Purpose

This project helps students understand how their raw scores on tests compare to a population. We do this by converting raw scores into percentile ranks using a machine learning technique called Quantile Transformation.

Visual Display

These scores are shown on a bar graph (using Chart.js) to visually represent relative performance.

Component Score Percentile
MCQ 20 88.5%
FRQ 4 77.2%
Total 82.8%

Takeaways

This project turns raw exam scores into meaningful insights using data transformation and visualization. It's especially useful for students preparing for standardized tests, helping them understand how they compare to others.

Roadmap

Features to add next:

  • Export percentile history
  • Upload custom score datasets
  • Train a model to directly classify scores into AP levels

Data Collection

We train the model using a dataset of prior student scores. Each record includes:

  • Multiple Choice Questions (MCQ) score
  • Free Response Questions (FRQ) score

Here’s a simplified example:

csv mcq,frq 12,3 18,4 20,4 22,5 15,2 10,1 24,5 17,3 19,4

In the real application, we load this dataset dynamically from a CSV file:

data_path = os.path.join(os.path.dirname(__file__), 'synthetic_data_science_scores.csv')
data = pd.read_csv(data_path)

Processing

To calculate percentiles, we use QuantileTransformer from scikit-learn, which maps raw scores to a uniform distribution:

n_samples = data.shape[0]
n_quantiles = min(1000, n_samples)

mcq_transformer = QuantileTransformer(n_quantiles=n_quantiles, output_distribution='uniform')
frq_transformer = QuantileTransformer(n_quantiles=n_quantiles, output_distribution='uniform')

mcq_transformer.fit(data[['mcq']])
frq_transformer.fit(data[['frq']])


This allows us to normalize student scores against the dataset, making it easy to calculate relative performance.

Backend API

Our Flask backend exposes an API endpoint where users submit their scores and receive percentiles in return:

@score_api.route('/api/percentile', methods=['POST'])
def calculate_percentile():
    scores = request.get_json()
    mcq_score = scores.get('mcq')
    frq_score = scores.get('frq')

    if mcq_score is None or frq_score is None:
        return jsonify({'error': 'Missing mcq or frq scores'}), 400

    score = Score(mcq_score, frq_score)
    return jsonify({
        'mcq_percentile': score.mcq_percentile(),
        'frq_percentile': score.frq_percentile()
    })

The Score class wraps the transformation logic:


class Score:
    def __init__(self, mcq, frq):
        self.mcq = mcq
        self.frq = frq

    def mcq_percentile(self):
        percentile = mcq_transformer.transform([[self.mcq]])[0][0]
        return float(np.round(percentile * 100, 2))

    def frq_percentile(self):
        percentile = frq_transformer.transform([[self.frq]])[0][0]
        return float(np.round(percentile * 100, 2))

Frontend Output

Once the user inputs scores and submits the form, the frontend calls /api/percentile and displays:

  • MCQ Percentile
  • FRQ Percentile
  • Combined estimate and predicted AP score (1–5)

Example Output

If the user enters:


{
  "mcq": 20,
  "frq": 4
}

The API may respond with:


{
  "mcq_percentile": 88.5,
  "frq_percentile": 77.2
}