6
\$\begingroup\$

I'm creating a data bank of MCQ (Multi Choice Questions) and their answers so that an app can be built around it. Regarding the actual storage format, I have two ideas:

  1. An array of objects with keys (q for question, a for choice-a, etc.).
  2. An array of arrays.

The first one is obviously more readable. Here is a brief sample of what I have so far:

{
    "data": [
        {
            "q": "What kind of language is Python?",
            "a": "Compiled",
            "b": "Interpreted",
            "c": "Parsed",
            "d": "Elaborated",
            "r": "b"
        },
        {
            "q": "Who invented Python?",
            "a": "Rasmus Lerdorf",
            "b": "Guido Van Rossum",
            "c": "Bill Gates",
            "d": "Linus Torvalds",
            "r": "b"
        }
    ]
}

The app will read the q key to print the question, then present the four options (a, b, c and d). And the last key (r) will store the right answer. This is very much readable when viewed as a JSON file also. However, what I am thinking is that once the data-bank grows in size into hundreds/thousands of QA, a lot of space will be wasted by those keys (q,a,b,etc.) isn't it? In this case, an array like this is more efficient from storage perspective:

{
    "data": [
        [
            "What kind of language is Python?",
            "Compiled",
            "Interpreted",
            "Parsed",
            "Elaborated",
            1
        ],
        [
            "Who invented Python?",
            "Rasmus Lerdorf",
            "Guido Van Rossum",
            "Bill Gates",
            "Linus Torvalds",
            1
        ]
    ]
}

In this case, each array will have 6 items viz. the question, four choices and finally the index of the correct choice (1==Interpreted, etc.).

Which of these two formats is better? Feel free to suggest any third format which is even better than these two.

\$\endgroup\$
9
  • 1
    \$\begingroup\$ @Sneftel FWIW, there are 4 sample Q&As , and you're probably trying to point out that the "answers" to both #3 and #4 have been incorrectly adapted when copy/paste editing was done to produce this prototype layout. Thank you for your eagle-eye observation. \$\endgroup\$
    – Fe2O3
    Commented Jun 13 at 10:41
  • 1
    \$\begingroup\$ @Fe2O3 Those answers are based on zero based indices (as computers usually understand). So in case of #3, "1" is the right answer as it refers to second index (Interpreted), "0" refers to the first index (Compiled) and will be incorrect. \$\endgroup\$ Commented Jun 13 at 10:46
  • 1
    \$\begingroup\$ @PrahladYeri I understand what you've just written. [0] is the question and [1-n] are its answers... Okay... There's one issue right there... Will all contributors understand this notation and implement it properly/consistently? Things are never as simple as they appear at first glance... Cheers! \$\endgroup\$
    – Fe2O3
    Commented Jun 13 at 10:51
  • 2
    \$\begingroup\$ Thinking like a programmer, I mentally put the question [0] in a separate variable and the four succeeding answers in a separate array of its own [0-3] while posting this data format! That's how the coding implementation usually goes but I get that it's not very intuitive from authoring perspective. \$\endgroup\$ Commented Jun 13 at 10:59
  • 2
    \$\begingroup\$ @Fe2O3 No, I mean, the first sample question is incorrect. Python is not only interpreted; it is bytecode compiled (though it would also be correct to say that it's parsed). \$\endgroup\$
    – Sneftel
    Commented Jun 13 at 11:19

4 Answers 4

3
\$\begingroup\$

"Feel free to suggest any third format which is even better than these two."

If the project is in the early stages of development, I would suggest keeping the data format as simple as possible.

Develop a trivial interface layer that goes in between your app and its stored data.
Development effort will be iterative, adding bells and whistles as those occur to you.

In keeping with that, the following suggests a simple "plain text" format for storing the data. Questions are fully left justified and answers are indented with one tab (or allow for any amount of disposable leading whitespace). Optionally, one or more blank lines can be inserted between questions. Perhaps allow for comments that could be used to mark regions.

I've already gone one step further, adding 'tags' to each question; 3 letters designating subject area. The digit indicates which of the answers is correct for the subject area of the question. (Perhaps "XXX" denotes Q&A not suitable for users under the age of 18.) Note: questions are not constrained to ALWAYS having 4 answers.

Once the app is walking and talking as you and co-developers wish it to, you can revisit the data storage scheme (and its interface) to grow to meet your users' needs. (Perhaps a version number, or a date, attached to each question will reduce user's boredom of being presented with questions they've already seen/answered.)

Let the app grow organically from simple origins. Don't let the tail wag the dog.

# Tested and working
Who invented Python?|ENT1|CSC2
    John Cleese
    Rasmus Lerdorf
    Guido Van Rossum
    Bill Gates
    Linus Torvalds

Who said "To be, or not to be"?|LIT1|CSC2|MUS3|PHL4
    Hamlet, Prince of Denmark
    George Boole
    Frank Sinatra
    Jean Paul Sarte
    All of the above
# End Tested and working

# New submission from Irene Jacobsen 2024.06.01
Definition of 'cleave'|LAN3
    To join together
    To cut into parts
    All of the above

Future Market Potential

An experienced educator might be interested in your app if it offered another feature: Imagine a well-crafted collection of 15-20 "high level" questions a student should answer correctly to review course material prior to final exams. Each Q's "incorrect" answer could vector off to another collection of 'simpler' Q&A designed to lead the student through the stages required to solve the "overview" question they've just gotten wrong.

2+2=5?! You got that one wrong.
Here, let's break it down into steps like so...

What is the value of 1+1?

Let your data store scheme accommodate future growth...

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thanks for the helpful insights! I especially like the simple and indented format with tagging and commenting feature included. I will try to incorporate these ideas in the final version. \$\endgroup\$ Commented Jun 13 at 10:49
  • 1
    \$\begingroup\$ @PrahladYeri Just some ideas to put on Post-It notes for the future. Notice that you want to start with "simple" until the app has most of the features you dream about now. If/when you're ready, a simple AWK script could convert 'simple' to 'JSON', or even to SQL INSERT statements for direct loading into a repository. I think it's easier to work with 'simple' as each new idea for the app comes to you in the shower... (Sometimes the muse is coy, waiting days/weeks before inspiring the million dollar innovation ideas.) Best wishes... \$\endgroup\$
    – Fe2O3
    Commented Jun 13 at 11:03
  • 1
    \$\begingroup\$ @PrahladYeri Years ago, I played with some HTML (4), CSS and went very shallow into Javascript for a project. That's all VERY dusty right now. However, I'd venture that I could have the above "data" suited-up and presentable for an interactive browser session in an hour or two of mucking-around... KISS! Keep It Simple, Stupid... ("Pride" just slows you down...) Cheers! \$\endgroup\$
    – Fe2O3
    Commented Jun 13 at 11:11
  • 1
    \$\begingroup\$ @PrahladYeri Just thinking about your project: Should questions (perhaps within a category) be presented in random sequence? Should they 'link' from one to the next in some instances? For any single Q&A, should the sequence of its answers be shuffled each time it is presented (for variety)? How to address 'internationalisation'; Q&A in several languages being represented "parallel" to one-another for completeness? Seems JSON array vs KV pairs is likely only the tip of the iceberg... :-) \$\endgroup\$
    – Fe2O3
    Commented Jun 13 at 11:38
9
\$\begingroup\$

You have at least three factors to judge the formats by:

  • Readability/intuitiveness (how easy are the formats to understand),
  • Extensibility (how readily do the formats adapt to unforeseen needs), and
  • Compactness (how much storage overhead is caused by the format).

How much each of these weighs depends on your use case. If it's important to you that outside contributors can read the format, then intuitiveness might be very important. If you expect the format to only be read by computers, with no need for humans to be able to parse the format, then compactness might be key.


I'd argue that each of your suggested formats scores poorly on all three of those factors:

  • Readability/intuitiveness: You use shorthand names for your keys, forcing you to explain what they're used for. For your array-based format, you're forced to explain what each position in the array means. An alternative would be to use slightly longer keys, which would make the format more-or-less self-explanatory. Compare

    {
      question: "What kind of language is Python?",
      answers: [
        "...",
        "...",
      ],
      correctAnswer: 0
    }
    

    to

    {
      "q": "What kind of language is Python?",
      "a": "...",
      "b": "...",
      "r": "b"
    }
    

    Which of these formats would you most easily understand? For the second format, would you be able to predict what happens when changing the values or keys, without knowing anything about the implementation details of whatever is parsing it?

  • Extensibility: Imagine in a few months time, when you stumble upon a use case for having multiple correct answers per question. Or for having many (26+) possible answer options. Or for attaching explanations to each question, so users will get a short blurp about why the answer is at it is. Or any of the muriad of other possible things that might change.

    For each of your formats this would prove a bit of a headache. Do you designate a new index/key for holding explanations? What do you do when you have so many questions that you reach "o": "...", "p": "...", "q": "..."? How many edge cases and special handling are you willing to implement in your parser?

    For a more extensible format, e.g. the example format from above, you can more easily attach new data to the existing format:

    {
      question: "What kind of language is Python?",
      explanation: "...",
      answers: [
        "...",
        "...",
      ],
      correctAnswer: [0, 1, 2],
    }
    
  • Compactness: Each of your example formats use JSON, leading me to think that storage efficiency isn't a huge deal for your use case. JSON has a fairly large overhead per value (compared to more compact binary formats like Protobuf or something homerolled), so if this is a key concern then I'd suggest going in a different direction. The upside of using JSON is that it's very flexible - so for your use case I think it's a good choice.


I'll assume that you want to use a JSON-based format. In that case I'd advise you to keep a few key points in mind:

  1. You should use the JSON data structures for their intended purpose. Objects are key-value pairs, and map a specified key to an arbitrary value. Don't use them for holding data where you don't know the keys (e.g. your list of answers). Similarly, arrays are for holding lists of things. Don't use them for holding values that have specific meaning (e.g. the question text for your questions).

  2. Favor extensibility over compactness. It'll give you far fewer headaches later on, when you need to change something.

  3. Map your mental model directly to the JSON structure, unless doing so will give you problems. This helps ensure intuitiveness. Most of us think of a multiple-choice question as a "Question" with one or more attached "Answers", of which one or more can be correct. So... use that as your data structure:

    { // <- Question
      "description": "...", // <- property of Question
      "answers": [
        { // <- Answer
          description: "...", // <- property of Answer
          correct: false
        },
        // ...
      ]
    }
    

    Doing so will make it much easier to parse, much easier to extend, and give you far fewer edge cases that you'll need special handling for.

As with all things in programming, there's always a degree of personal opinion involved.
Personally, I would probably lean towards the first example format ({ question: "...", answers: ["..."], correctAnswer: [0] }) if I knew that this wouldn't expand into a highly complex application over time, and it was important for me that human readers can easily parse and edit the format.
I'd favor the last format ({ "description": "...", "answers": [{ "description": "...", correct: false }] }) if I expected the majority of the work to be done by computers, I expected the format to change over time, or I'd like a more direct mapping to the implementation that I'd expect to make in my parser.

\$\endgroup\$
2
  • \$\begingroup\$ Me? I "think of a multiple-choice question as a "Question" with two or more attached "Answers""... :-) \$\endgroup\$
    – Fe2O3
    Commented Jun 13 at 10:46
  • 1
    \$\begingroup\$ I suggest the name choice for what you and OP are calling an answer. IMO the “answer” is the choice selected by the respondent. This matches the usage in some of the MCQ libraries I use. The thing is a called a multiple choice question, after all, not a “multiple answer question”. \$\endgroup\$ Commented Jun 14 at 16:11
5
\$\begingroup\$

Don't worry about amount of storage consumed.

Which of these two formats is better?

Whichever better supports your ability to manually review entries and create new ones. An item which you've already answered:

The first one is obviously more readable.

Go with the first one.


But what if we have a Success Disaster, with many collaborators contributing thousands of questions? Don't worry about it.

At that point, you'll have plenty of time to e.g. convert the 1st format into the 2nd. Or more likely, into .CSV so you can use tools like Google Sheets to collaborate.

Looking at those initial questions, conservatively we can fit at least four of them per KiB, without even gzipping the JSON indent SPACE characters. Suppose we have a RAM budget of a megabyte. Then we can scale to at least four thousand questions with no issue.

Imagine we're growing. And we don't want to allocate e.g. 1 GiB. Then the obvious thing to do is import those questions into some convenient datastore such as an RDBMS. Just insert a few thousand rows using sqlite and you're done. The app now relies on external storage managed by some DB vendor, rather than relying on internal storage managed by malloc. Problem solved.

There's no reason to implement such things now. Wait until the need arises, confident that you have a good Plan B to pull out of your back pocket when storage needs continue to grow.

\$\endgroup\$
3
\$\begingroup\$

I'll consider this question abstractly from a data modeling perspective. Asume for each question-choices-answer tuple, there's one question q, there's exactly 4 answers a-d, then one answer r. In this case, you can use a single table, where each row represents one tuple and each column represents part of that tuple.

q a b c d r
What kind of language is Python? Compiled Interpreted Parsed Elaborated b
Who invented Python? Rasmus Lerdorf Guido van Rossum Bill Gates Linus Torvalds b

The benefit of this is that there's one row per question-choices-answer tuple and it's easy to understand. However, it would not accommodate a question with 3 answers without doing something with the leftover column, and 5 or more answers don't fit. Usually it's not desirable to have NULLs or arbitrarily many columns.

Instead of this wide format, you could also imagine pivoting to a long format, where each row stores tuple ID, variable, and value.

Tuple ID Key Value
1 q What kind of language is Python?
1 a Compiled
1 b Interpreted
1 c Parsed
1 d Elaborated
1 r b
2 q Who invented Python?
2 a Rasmus Lerdorf
2 b Guido van Rossum
2 c Bill Gates
2 d Linus Torvalds
2 r b

This is less intuitive to read, since the tuple is split over 6 rows, but it does allow for arbitrary numbers of answers per tuple. You may or may not want that. This also makes it easier to do group by and aggregate operations, such as if you wanted to calculate the average length of answers a, answers b, ... you could do GROUP BY variable name, apply a function, and use aggregation function. In the wide format you'd need to name each column explicitly, or use fancy column selection tricks like tidyselect has.

Which one you use depends on your problem and use case. Is human readability important? Is supporting arbitrary number of answers important? Do you even want that possibility? Hadley Wickham goes through these concepts in his tidy data paper. More formally, these rectangular tables are a common form of the relational model in storing data. Either table can be stored efficiently in relational SQL databases, and are highly optimized. There are whole textbooks on relational model theory and especially normalizing data to eliminate redundancy and ensure certain kinds of data integrity, but I figured I'd write some introduction.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.