SurveyLoopr
Tips & Tricks
13 min read

Weighted Score in XLSForm: Totals, Means and Indexes

Mehrab Ali
Mehrab AliPublished October 26, 2026

To calculate a weighted score in XLSForm, add a calculate row that multiplies each answer by its weight and adds them up: (${staples_days} * 2) + (${pulses_days} * 3) + ${veg_days}. For select_one questions, make the choice code the score (1 to 5), so the answer can go straight into the sum. The examples below cover totals, weights, Likert means that skip "Don't know", reverse-scored items, select_multiple scores and 0-100 indexes, and each one compiles with pyxform and ODK Validate, the same check ODK Central runs when you upload a form.

Download example form (.xlsx)All seven scoring examples in one XLSForm, one group each. Tested with pyxform and ODK Validate; free to reuse under CC BY 4.0.Download

How do I add up the scores of select_one questions?

Give each choice the score as its name in the choices sheet, then add the answers:

typenamelabelcalculationrequired
select_one satsat_waitingHow satisfied were you with the waiting time at the clinic?yes
select_one satsat_staffHow satisfied were you with how the staff treated you?yes
select_one satsat_medicineHow satisfied were you with the availability of medicines?yes
calculatesat_total${sat_waiting} + ${sat_staff} + ${sat_medicine}
noteshow_sat_totalSatisfaction score: ${sat_total} out of 15
surveychoicessettings
list_namenamelabel
sat1Very dissatisfied
sat2Dissatisfied
sat3Neither satisfied nor dissatisfied
sat4Satisfied
sat5Very satisfied
surveychoicessettings

A select_one answer is stored as text, '4', but +, -, * and div turn it into a number, so no conversion is needed. This only works when the choice names are numbers. If your list uses names such as satisfied, the sum is not a number and nothing warns you, which is why numeric codes are the safer habit for any list you might score.

How do I give questions different weights?

Multiply each answer by its weight. The WFP Food Consumption Score is the standard example: days each food group was eaten in the past 7 days, times a fixed weight per group:

typenamelabelcalculationconstraintconstraint_message
integerfcs_staplesIn the past 7 days, on how many days did the household eat cereals, roots or tubers?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_pulses...pulses, beans or nuts?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_veg...vegetables or leaves?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_fruit...fruit?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_meat...meat, fish or eggs?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_milk...milk or dairy products?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_sugar...sugar or sweets?. >= 0 and . <= 7Enter 0 to 7 days
integerfcs_oil...oil or fat?. >= 0 and . <= 7Enter 0 to 7 days
calculatefcs_score(${fcs_staples} * 2) + (${fcs_pulses} * 3) + ${fcs_veg} + ${fcs_fruit} + (${fcs_meat} * 4) + (${fcs_milk} * 4) + (${fcs_sugar} * 0.5) + (${fcs_oil} * 0.5)
calculatefcs_groupif(${fcs_score} <= 21, 1, if(${fcs_score} <= 35, 2, 3))
noteshow_fcsFood Consumption Score: ${fcs_score} (group ${fcs_group}: 1 = poor; 2 = borderline; 3 = acceptable)
surveychoicessettings

The brackets are not required, because * runs before +, but they make each weight easy to check against the indicator's guidance. Write decimal weights with a dot (0.5), whatever your computer's regional settings are. The constraint keeps every input between 0 and 7, so the score cannot go past its maximum of 112. The full module with WFP's notes is in standardized survey modules.

How do I calculate a Likert mean that skips "Don't know"?

Code "Don't know" as -99, then count only the answers above 0 and divide by that count:

typenamelabelcalculationrequired
select_one agreetrust_leadersCommunity leaders act in the interest of everyone.yes
select_one agreetrust_policeThe police treat people fairly.yes
select_one agreetrust_healthThe health centre gives good care.yes
calculatetrust_sumif(${trust_leaders} > 0, ${trust_leaders}, 0) + if(${trust_police} > 0, ${trust_police}, 0) + if(${trust_health} > 0, ${trust_health}, 0)
calculatetrust_answeredif(${trust_leaders} > 0, 1, 0) + if(${trust_police} > 0, 1, 0) + if(${trust_health} > 0, 1, 0)
calculatetrust_meanif(${trust_answered} > 0, round(${trust_sum} div ${trust_answered}, 2), '')
noteshow_trustMean trust score: ${trust_mean} (from ${trust_answered} of 3 statements)
surveychoicessettings
list_namenamelabel
agree1Strongly disagree
agree2Disagree
agree3Neither agree nor disagree
agree4Agree
agree5Strongly agree
agree-99Don't know
surveychoicessettings

Someone who answers 5, 4 and "Don't know" gets a mean of 4.5 from 2 statements, not 3 from 3. The if(${trust_answered} > 0, ...) guard leaves the mean empty when every answer is "Don't know", instead of dividing by zero. There is no average() or mean() function in XLSForm: a mean is always a sum div a count.

Warning:

Never add a -99, -98 or -96 code straight into a score. One "Don't know" lowers the total by 99, the form accepts it without a warning, and the error only shows up in analysis.

How do I reverse-score an item?

For a 1 to 5 scale, a reversed item scores 6 - ${q}, so 1 becomes 5 and 5 becomes 1. For a 1 to 4 scale, use 5 - ${q}:

typenamelabelcalculationrequired
select_one agreecope_manageI can handle the problems that come up in my work.yes
select_one agreecope_overwhelmedI often feel overwhelmed at work.yes
select_one agreecope_supportI can ask a colleague for help when I need it.yes
select_one agreecope_give_upI feel like giving up on difficult tasks.yes
calculatecope_totalif(${cope_manage} > 0, ${cope_manage}, 0) + if(${cope_overwhelmed} > 0, 6 - ${cope_overwhelmed}, 0) + if(${cope_support} > 0, ${cope_support}, 0) + if(${cope_give_up} > 0, 6 - ${cope_give_up}, 0)
noteshow_copeCoping score: ${cope_total} out of 20 (higher is better)
surveychoicessettings

The two negative statements are reversed so that a higher score always means better coping. Keep the choice list the same for every item and reverse in the calculation, not in the choices sheet: two lists with the same labels and opposite codes are hard to spot in the data and easy to analyse the wrong way round.

How do I score a select_multiple question?

Use selected() for each option that carries weight, or count-selected() when every option scores 1:

typenamelabelcalculation
select_multiple assetshh_assetsWhich of these does the household own?
calculateasset_scoreif(selected(${hh_assets}, '1'), 1, 0) + if(selected(${hh_assets}, '2'), 2, 0) + if(selected(${hh_assets}, '3'), 3, 0) + if(selected(${hh_assets}, '4'), 5, 0)
calculateasset_countif(selected(${hh_assets}, '-96'), 0, count-selected(${hh_assets}))
noteshow_assetsAsset score: ${asset_score} (${asset_count} assets)
surveychoicessettings
list_namenamelabel
assets1Radio
assets2Mobile phone
assets3Bicycle
assets4Motorbike
assets-96None of these
surveychoicessettings

You cannot add a select_multiple answer directly: its value is a list such as 1 3 4, not a number. selected() checks one option at a time, so each option can have its own weight. For the "None of these" option and its constraint, see skip logic on select_multiple.

How do I keep the score when some questions are skipped?

A skipped question is empty, and anything plus empty is not a number, so the whole score disappears. Wrap every question that can be skipped in coalesce(${q}, 0). This livestock example converts animal counts into Tropical Livestock Units (TLU), with cattle at 0.7, goats and sheep at 0.1 and chickens at 0.01:

typenamelabelcalculationrelevantconstraint
select_one yes_noowns_livestockDoes the household own any livestock?
integern_cattleHow many cattle?${owns_livestock} = '1'. >= 0
integern_goatsHow many goats or sheep?${owns_livestock} = '1'. >= 0
integern_chickensHow many chickens?${owns_livestock} = '1'. >= 0
calculatetluround((coalesce(${n_cattle}, 0) * 0.7) + (coalesce(${n_goats}, 0) * 0.1) + (coalesce(${n_chickens}, 0) * 0.01), 2)
noteshow_tluLivestock holding: ${tlu} TLU
surveychoicessettings
list_namenamelabel
yes_no1Yes
yes_no0No
surveychoicessettings

A household without livestock gets 0 TLU instead of an empty value, and anything that reads ${tlu} later, such as a follow-up question or a classification, keeps working. The XLSForm calculations hub has more on coalesce().

Note:

Only use 0 when 0 is the true answer. A skipped livestock question really means no animals. A skipped Likert item does not mean a score of 0; count it out of the mean as in the "Don't know" example above.

How do I turn a score into a 0-100 index and a category?

Divide by the maximum possible score, multiply by 100 and round. Then classify the result with if():

typenamelabelcalculation
select_one yes_nowash_latrineDoes the household use an improved latrine?
select_one yes_nowash_soapIs there soap and water at the handwashing place?
select_one yes_nowash_treatDoes the household treat its drinking water?
select_one yes_nowash_storageIs drinking water stored in a covered container?
calculatewash_raw(${wash_latrine} * 3) + (${wash_soap} * 3) + (${wash_treat} * 2) + (${wash_storage} * 2)
calculatewash_indexround(${wash_raw} div 10 * 100, 0)
calculatewash_levelif(${wash_index} >= 80, 'good', if(${wash_index} >= 50, 'fair', 'poor'))
noteshow_washWASH index: ${wash_index} out of 100 (${wash_level})
surveychoicessettings

Because Yes is coded 1 and No 0, each answer times its weight is the points it earns, and the maximum is the sum of the weights, here 10. An index on a 0-100 scale is easier to compare across modules with different numbers of questions, and the category can drive a follow-up question in the relevant column, for example ${wash_level} = 'poor'. For more ways to classify with if(), see the XLSForm if statement guide.

Common scoring mistakes and how to fix them

You wroteWrite insteadError you will see
sum(${q1}, ${q2})${q1} + ${q2}cannot handle function 'sum'. Requires 1 arguments but 2 provided.
SUM(${q1})${q1} + ${q2} (functions are lowercase)cannot handle function 'SUM'
average(${q1}, ${q2})(${q1} + ${q2}) div 2cannot handle function 'average'
(${q1} + ${q2}) / 2div 2Unexpected beginning of path in expression
${d1} * 0,5${d1} * 0.5Invalid calculate for the bind attached to "${s}" : Bad node
if(${q1} = '5', 1)if(${q1} = '5', 1, 0)cannot handle function 'if'. Requires 3 arguments but 2 provided.
${q1} + ${q_2} with no q_2 rowUse the question's exact nameCould not find the name 'q_2'
${q1} + ${q2} when q2 can be skippedcoalesce(${q2}, 0)No error, but the score is empty
${q1} + ${q2} when "Don't know" is -99if(${q2} > 0, ${q2}, 0)No error, but the score drops by 99

The last two upload without complaint, so preview the form and answer it once with a skipped question and once with "Don't know" before you deploy. For other messages, see common XLSForm errors, or paste your form into the free XLSForm validator.

Does this work the same in ODK, KoboToolbox and SurveyCTO?

The expressions are the same on all three. What differs:

PlatformWhat to know
ODK (XLSForm)Calculate rows with +, -, *, div, if(), coalesce(), selected() and count-selected(), as above.
KoboToolboxSame XLSForm syntax. In the Formbuilder, add a Calculate question and type the expression; to make the choice code the score, set each option's XML value to the number.
SurveyCTOSame functions in calculate fields. Skip logic goes in a column named relevance, not relevant.

On every platform the score is saved as its own column in the export, next to the answers it came from, so you can recheck it in Excel, Stata or R.

Why calculate scores in the form

A score calculated during the interview can drive the rest of it: a poor Food Consumption Score can open a referral question, and a low satisfaction score can ask what went wrong while the respondent is still there. Each score is calculated the same way on every phone, so there is no manual tallying, and supervisors see the result in the first day's data instead of after cleaning.

Tip:

You can describe it in plain language instead: "add up the five satisfaction questions, skip Don't know answers, and show the mean". LooprAI writes the calculate rows with the right weights and guards, then compiles the form with pyxform before anything is saved, so a slash instead of div or a missing coalesce() never reaches your form.

Build my first form free

Next: XLSForm calculation examples →

Frequently Asked Questions

How do I calculate a weighted score in XLSForm?

Add a calculate row and multiply each answer by its weight, then add them: (${staples_days} * 2) + (${pulses_days} * 3) + ${veg_days}. For select_one questions, make each choice code the score so the answer can be used directly.

Can I add select_one answers together?

Yes, if the choice names are numbers. The answer is stored as text such as '4', and +, -, * and div convert it to a number. With text choice names such as satisfied, the result is not a number and no error is shown.

Is there an average() function in XLSForm?

No. average() and mean() fail with cannot handle function. Calculate a mean as a sum divided by a count with div, for example round(${trust_sum} div ${trust_answered}, 2), and guard against a count of zero with if().

Why is my total score blank?

One of the questions in the sum was skipped, and an empty value makes the whole result empty. Wrap each question that can be skipped in coalesce(${q}, 0), but only where 0 is the true answer for a skipped question.

How do I leave Don't know out of a Likert score?

Code Don't know as -99, add each answer with if(${q} > 0, ${q}, 0), and count valid answers with if(${q} > 0, 1, 0). Divide the sum by that count, so Don't know neither adds to the score nor counts in the mean.

How do I score a select_multiple question?

Use selected() for each option: if(selected(${hh_assets}, '2'), 2, 0) + if(selected(${hh_assets}, '3'), 3, 0). Each option can have its own weight. When every option is worth 1, count-selected(${hh_assets}) gives the number of options chosen.

No Credit Card Required

Build the form first. Add hosting when you need it.

Start free with LooprAI, then deploy a managed ODK Central server or run DataSnap checks when your project is ready.

Automation handles the mechanical work.

Researchers decide what the evidence means.