3
\$\begingroup\$

I was on Hacker Rank for the first time and there was a question that required to get the inputs from STDIN and use collections.namedtuple(). It was my first time working with getting inputs from STDIN so the way I acquired the inputs and used them to calculate the data might not be efficient.

The question was to calculate the average mark of the students and was given a multiple line string input with specific data about the student (like: NAME, CLASS, ID) in which one of them is MARKS (I have added an image of a sample STDIN input data below to explain better). But the layout of the input data could change so the code must be able to figure out where the MARKS are in the data and also get the number of students in order to calculate the average of their marks.

I came up with a quick way to solve this but was wondering how to appropriately and efficiently acquire the input data and perform the calculation.

Briefly, what is a better (pythony, efficient, shorter) way to write my code?

# Enter your code here. Read input from STDIN. Print output to STDOUT
from collections import namedtuple

import sys

data = [line.rstrip().split() for line in sys.stdin.readlines()]

sOMCache = [];
n = 0

dataLength = len(data)

if 'MARKS' in data[1] : 
    marksIndex = data[1].index('MARKS')
    for i in range(dataLength) :
        if n > 1 :
            sOMCache.append(data[n][marksIndex])
        n += 1 

sOM = sum([int(x) for x in sOMCache])

Point = namedtuple('Point','x,y')
pt1 = Point(int(sOM), int((data[0][0])))
dot_product = ( pt1.x / pt1.y )
print (dot_product)

Samples

Testcase 01:

5
ID         MARKS      NAME       CLASS     
1          97         Raymond    7         
2          50         Steven     4         
3          91         Adrian     9         
4          72         Stewart    5         
5          80         Peter      6   

Expected output: 78.00


Testcase 02:

5
MARKS      CLASS      NAME       ID        
92         2          Calum      1         
82         5          Scott      2         
94         2          Jason      3         
55         8          Glenn      4         
82         2          Fergus     5

Expected output: 81.00

\$\endgroup\$
1
  • \$\begingroup\$ @200_success oh so thats what they meant by change the img to text data. Thanks for the edit. \$\endgroup\$
    – seriously
    Nov 8 '21 at 22:24
5
\$\begingroup\$

I liked the fact that you used the sum() builtin function, but I have no idea what sOM or sOMCache mean in sOM = sum([int(x) for x in sOMCache]) — "Sum of marks", maybe? But the capitalization is weird by Python standards, and this isn't a cache — you wouldn't just eject data from this "cache", would you?

I think, as you probably suspected, that you missed the mark for this exercise. The main issues with your solution are:

  • Misunderstanding the Point. What you're computing here is an average, not a dot product. The dot product in the tutorial was just to illustrate how you can access member fields within a namedtuple using .x or .y.
  • Failure to take advantage of namedtuple. If you do things right, you should be able to write something like row.MARKS to get the value of the MARKS column for a particular row.
  • Lack of expressiveness, such that it's not obvious what the code intends to do at a glance.

Suggested solution

from collections import namedtuple
from statistics import mean
import sys

def records(line_iter):
    """
    Read records, one per line, space-separated, following a header.
    
    The header consists of two lines: the first line contains an integer
    specifying the number of records, and the second line specifies the
    column names.  Records are yielded as namedtuples, where the fields
    have names specified by the second header row.
    """
    n = int(next(line_iter))
    Record = namedtuple('Record', next(line_iter))
    for _ in range(n):
        yield Record(*next(line_iter).split())

print(mean(float(rec.MARKS) for rec in records(sys.stdin)))

Explanation:

The final line of code drives all the work: read records from STDIN, and print the mean of the MARKS column. I've chosen to interpret the marks as floats for versatility, but if you're sure that the data are all integers then you could call int(rec.MARKS) instead.

All of the work to read the input is handled by a generator function. The docstring describes what it does. As you can see, it takes advantage of the namedtuple to let you refer to the column of interest later.

\$\endgroup\$
0
4
\$\begingroup\$

there was a question that required to get the inputs from STDIN and use collections.namedtuple()

Really? I don't think that HackerRank would enforce using a named tuple, and using a named tuple isn't hugely helpful here. Plus, if you were to use a named tuple, I would sooner expect that it capture the four fields in every line rather than one point at the end.

Python doesn't need semicolons ; at the end of statements so you can delete that.

The first line doesn't need to be split at all. The second line does need a full split, but every line after that only needs marksIndex + 1 splits which you can specify via the maxsplit parameter.

Your if 'MARKS' in check is a little odd. Hacker Rank hasn't asked for validation, and if they did, they are many more things you'd want to add to your validation than just this.

Keeping data in one big list of lists will occupy much more memory than just keeping a running total.

When you say dot product... what you have isn't really a dot product. Or as a stretch, maybe it's the dot product of two length-one vectors where the first value is 1/n and the second is the total. This doesn't make a whole lot of sense. If the dot product did appear in this question, I would expect two vectors, one full of 1/n and the other with the student marks; then the dot product would find the sum of the products of each element. But this is not an efficient way to implement this question. Just have a running total.

I usually provide a suggested implementation at the end of review, but here I think there's more value for you to work on the above and come up with it on your own.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ probable context \$\endgroup\$ Nov 8 '21 at 18:48
  • \$\begingroup\$ I think you can see from @200_success's answer how it's possible to get some mileage out of using namedtuple to handle things. \$\endgroup\$
    – martineau
    Nov 9 '21 at 2:17
  • \$\begingroup\$ @martineau I understand how it can be used, but in this case the question is only looking for one field - and an aggregate of one field, at that. So a simple solution doesn't really need full records. \$\endgroup\$
    – Reinderien
    Nov 9 '21 at 3:27
  • \$\begingroup\$ It's not that inefficient to yield full records from the generator function. As long as the caller never stores the full records in a list, it can extract the MARKS field and discard the rest of the record as it streams through each row. \$\endgroup\$ Nov 9 '21 at 7:02
2
\$\begingroup\$

namedtuple implementation

It's possible that I misunderstand the purpose of the exercise but since you are processing a list of students, I would expect that the namedtuple would represent a student object with properties like name, class, marks etc. You are still dealing with list indexes, and your present implementation of namedtuple does not really provide the flexibility you could have achieved.

Below is a proposed snippet. To simplify a bit, I would first process the input of stdin line by line, and then split each line in a loop. We first need to find the column headers so we discard the previous line(s). As soon as we know the column headers (student properties) we can create the namedtuple dynamically (its properties are not determined in advance and could change), and we can start processing the data.

What we are effectively doing is this:

Student = namedtuple('Student', ['ID', 'MARKS', 'NAME', 'CLASS'])

There is one thing you should do: verify that after splitting the line contains the number of elements you expect, that is 4. Then we add a Student instance (the namedtuple) to a list for later tallying.

To add a new Student instance we should normally do something like this:

student = Student('1', '97', 'Raymond', '7')

but instead we want to pass a list of values (that is, the line split). So the trick is to pass the arguments with a single *.

In this example student.NAME would return Raymond and student.MARKS would return 97.

Accordingly, the proposed code would be something along these lines:

import statistics
from collections import namedtuple

lines = [line.rstrip() for line in sys.stdin.readlines()]

students = []
student_fields = None

for i, line in enumerate(lines, start=1):
    elements = line.split()
    # this line contains column headers
    if 'MARKS' in elements:
        print(f"Column headers found on line {i}: {elements}")
        student_fields = elements
        Student = namedtuple('Student', student_fields)
    else:
        if student_fields:
            student = Student(*elements)
            students.append(student)

# compute average of all students
average_marks = statistics.mean([int(student.MARKS) for student in students])
print(f"Average marks: {average_marks}")

Calculating the average is easy but in this case I have used statistics.mean for a more straightforward approach. It is a simple list comprehension. NB: check that the list is not empty before computing average.

This is a rough approach that assumes that the data is well-formed, that the number of elements is always correct, and that the marks can be cast as integer.

Without the requirement to use a namedtuple the Python csv module could have done the trick perhaps, with a Dictreader.

Coding style

Suggested reading is PEP8 for recommended Python coding style. Variable names should be lowercase, underscore can be used to separate keywords.

dot_product = ( pt1.x / pt1.y )

should be written:

dot_product = (pt1.x / pt1.y)

And

for i in range(dataLength) :

becomes:

for i in range(dataLength):
\$\endgroup\$
3
  • \$\begingroup\$ Ish. In your proposed code: I would drop the enumerate; you shouldn't construct a list for mean and should just pass the generator; the fields of your Student should be lowercase; and it doesn't make much sense to search every single line for the header. The header is defined to appear on the second line. \$\endgroup\$
    – Reinderien
    Nov 8 '21 at 14:47
  • \$\begingroup\$ I am not certain that the headers will always be found on the second line actually. Hence this naive implementation. \$\endgroup\$
    – Kate
    Nov 8 '21 at 14:52
  • 1
    \$\begingroup\$ OK, but... your implementation will now break for a file that contains a student named MARKS. The best information that we have is that the headers appear on the second line and nowhere else. \$\endgroup\$
    – Reinderien
    Nov 8 '21 at 14:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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