7
\$\begingroup\$

My background is medical, but I required a program that calculated whether my patients are "normal" or 1-2 SD values away from the mean. The program works just fine, but I'm interested how I can do better. (This all was made after I started learning python a month ago, so please be kind)

I'm particularly interested in making the standard deviation calculation process a bit more easy to add in and remove or maybe some way to make it more modular overall? (The part with all that mean_list and sd_list stuff)

Currently, I have to manually enter mean and std deviations into lists and make sure the format is always right. On top of that, I have to create individual functions for every test I've got. While this has benefits, like managing the tests with exceptions, different number of results or entirely different result system; most of the code is just repetition of itself. Which is not particularly desired. (Especially I'm near 1700 lines and at least 1/3-1/4 of it is repetition)

def csvWriter(patient_admin, patient_ID, patient_age, patient_sex, patient_edu, test_name, printable_list):
    import csv
    from time import strftime
    date = strftime("%Y-%m-%d")
    time = strftime("%H:%M:%S")
    with open(test_name, 'a', encoding='ISO-8859-9', newline='') as csvfile:
        data_writer = csv.writer(csvfile, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
        data = ["", patient_admin, date, time, patient_ID, patient_age, patient_sex, patient_edu] + printable_list
        data_writer.writerow(data)
        csvfile.close()

def txtWrite(patient_admin, patient_ID, console_results):
    from time import strftime
    date = strftime("%Y-%m-%d")
    file_name = str(patient_ID) + "-(" + date + ")-" + patient_admin + ".txt"
    with open(file_name, 'a', encoding='UTF-8') as file:
        file.write("\n" + console_results)
        file.close()

def txtCreate(patient_admin, patient_ID, patient_age, patient_sex, patient_edu):
    #creates a txt file with desired format for the physician
    from time import strftime
    date = strftime("%Y-%m-%d")
    time = strftime("%H:%M:%S")
    file_name = str(patient_ID) + "-(" + date + ")-" + patient_admin + ".txt"
    patient_name = input("Hastanın ismi: ")
    with open(file_name, 'w', encoding='UTF-8') as file:
        file.write("Testi uygulayan: " + patient_admin + "\nGünün tarihi: " + date + "\nSaat: " + time +
        "\nHastanın ismi: " +  patient_name + "\nHastanın kodu: " + str(patient_ID) +
        "\nHastanın yaşı: " + str(patient_age) + "\nHastanın cinsiyeti: " + str(patient_sex) +
        "\nHastanın toplam eğitim yılı: " + str(patient_edu) +
        "\n=============================================\n")
        file.close()

def inputPatient_age():
#a simple loop to get patient's age  
    while True:   
        try:        
            patient_age = int(input("Hastanın yaşı: "))
        except:            
            print("Lütfen sadece rakam giriniz.")
            continue
        if patient_age < 0:
            #ensures the number is bigger than 0
            print("Lütfen 0'dan büyük bir sayı giriniz.")
            continue
        else:
            break
    return patient_age

def inputPatient_sex():   
#a simple loop to get the sex of the patient, provides two options, female or male
    while True:    
        patient_sex_user_input = input("Hastanın cinsiyeti: (1) Kadın - (2) Erkek: ")
        if patient_sex_user_input == "1":
            patient_sex = "Kadın"
            break
        elif patient_sex_user_input == "2":
            patient_sex = "Erkek"
            break
        else:
            print("Lütfen sadece 1 veya 2 giriniz.")
            continue
    return patient_sex

def inputPatient_edu(): 
#a simple loop to get the number of years of education
    while True:
        try:
            patient_edu = int(input("Hastanın toplam eğitim yılı: "))
        except:
            print("Lütfen sadece rakam giriniz.")
            continue
        else:
            break
    return patient_edu


def mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests):
    try:
        menu_list = [testStroop, testVf, testSf, testSbst, testTm,
                     testMmt, testCd, testMoca, testEcr, testWisconsin, testWechsler, testRkft, testSdot, testVvt]  
            #list of tests' function names

        test_name_list = ["(1)Stroop ", "(2)Sözel akıcılık ", "(3)Semantik Akıcılık ",
                          "(4)SBST ", "(5)İz sürme ", "(6)Mini Mental test ", "(7)Saat çizme ",
                          "(8)MOCA ", "(9)Artırılmış İpuçlu Hatırlama ", 
                          "(10)Yetişkin Wisconsin Kart Eşleme ", "(11)Wechsler Zeka testi ",
                          "(12)Rey Karmaşık Figür Testi ", "(13)Çizgi yönünü belirleme testi ",
                          "(14)Visual Verbal Test"] 
            #verbal names of the tests

        menu_ui = ("==================================================\n") 
        for i in range(len(test_name_list)):
            if (i+1)%3 == 0:
                menu_ui = menu_ui + test_name_list[i] + "\n"
            else:
                menu_ui = menu_ui + test_name_list[i]                
        menu_ui = menu_ui + "\n"   
        print(menu_ui)
        #creates and prints the mainMenu

        print("Şu ana kadar yapılan testler: ")
        print(menu_done_tests)
        #prints tests that were done so far (could be made into a string, and printed)        

        menu_input = int(input("Girmek istediğiniz testin numarasını giriniz veya çıkış için (" + str(len(menu_list)+1) + ") giriniz: " ))        
        #input the test number you want to use, or press the calculated number (one higher than the test number) to exit
        if menu_input in menu_done_tests:
            x = input("Bu test zaten girilmiş, tekrar girmek istiyor musunuz? (e)vet/(h)ayır: ")
            if x == "E" or x == "e":
                print("Bu test daha öncesinde yapıldığı için, veri bankasında birden fazla girişe" +
                " neden olacaktır, lütfen sonrasında düzeltmeyi unutmayınız.")
 #asks the user if he administered the test before and continues only if approved,
#also this creates a duplicate results in the files and prompts the user to remove it manually later
            else:
                print("Bir önceki basamağa geri dönülüyor.")
                mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests)
                #returns to the previous step

        if menu_input <= len(menu_list):
            menu_done_tests.append(test_name_list[menu_input-1])
            menu_list[menu_input-1](patient_ID, patient_age, patient_edu, patient_sex, patient_admin)
            mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests)
            #using the number, calls the function user wanted, adds that to the done tests
        elif menu_input == (len(menu_list)+1):
            print("\nİşlem iptal edildi, şu ana kadar yapılanlar kaydedildi. " +
            "Program baştan başlatılıyor.")
            mainStartup()
            #this is the exit command

        elif (len(menu_list)+1) < menu_input:
            print("\nLütfen listede olan numaralardan giriniz.")
            mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests)
            #this is called if user enters a number higher than tests that are in the program
    except:
        print("\n Lütfen sadece rakam giriniz.")
        mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests)
        #DEATH PROTECTION TOME

def mainStartup(): #the mainStartup function
    print("\n===========================================\n" + 
    "Akbulut Standart Sapma Hesaplayıcı v0.4.1" +
    "\n===========================================\n")    

    patient_admin = input("Testi uygulayan kişi: ") #the person who administrated the test
    patient_ID = input("Hastanın kodu: ")
    patient_age = inputPatient_age()
    patient_sex = inputPatient_sex()
    patient_edu = inputPatient_edu()
    #gets input for all the data program needs
    menu_done_tests = []
    #creates a list for the tests that were employed before, so it doesn't get used again

    print("=======================================")    
    user_input = input("Yukarıdaki bilgileri onaylıyor musunuz? (e)vet/(h)ayır: ")
    #a step to ensure correct info was entered
    if user_input == "E" or user_input == "e":
        txtCreate(patient_admin, patient_ID, patient_age, patient_sex, patient_edu)
        #creates a txt file for future data entry
        mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests)
        #starts the program proper
    else:
        print("Program tekrar başlatılıyor.")
        mainStartup()
        #resets

def calcSd_int(result_list, mean_list, sd_list):
    #finds out which SD interval patient result is in and orders it in a list
    std_dev_output_list = []
    for i in range(len(result_list)):
        sd = ((result_list[i] - mean_list[i]) / sd_list[i])
        std_dev_output_list.append(float("%.2f" % sd))
    return std_dev_output_list

def outputPrintlist(result_list, std_dev_output_list, std_dev_verbal_list):
    #puts all the lists in their proper, more manageable order to print in CSV
    printable_list = []
    for y in range(len(result_list)):
        printable_list.append(result_list[y])
        printable_list.append(std_dev_output_list[y])
        printable_list.append(std_dev_verbal_list[y])
    return printable_list

def outputConsole_results(result_list, std_dev_output_list, std_dev_verbal_list):
    #gets the results ready to print onto the screen
    console_result = []
    for i in range(len(std_dev_output_list)):
        console_result.append("Hastanın puanı: " + str(result_list[i]) + ", " + 
        str(std_dev_verbal_list[i]) + ", " + str(std_dev_output_list[i]) + "SD")
    return console_result


def calcSD_less_better(std_dev_output_list): 
    #If lower score means better score, this is used to turn the SD floats into verbal results and lists it
    std_dev_verbal_list = []

    for i in range(len(std_dev_output_list)):
        if std_dev_output_list[i] <= 1:
            x = "Normal."
        elif std_dev_output_list[i] > 1 and std_dev_output_list[i] <= 2:
            x = "Hafif bozulma."
        elif std_dev_output_list[i] > 2 and std_dev_output_list[i] <= 3:
            x = "Orta derecede bozulma."
        else:
            x = "Ağır bozulma."    
        std_dev_verbal_list.append(x)

    return std_dev_verbal_list

def calcSD_more_better(std_dev_output_list):
#If higher score means better score, this is used to turn the SD floats into verbal results and lists it
    std_dev_verbal_list = []
    for i in range(len(std_dev_output_list)):
        if std_dev_output_list[i] >= -1:
            x = "Normal."
        elif -2 <= std_dev_output_list[i] < -1:
            x = "Hafif bozulma."
        elif -3 <= std_dev_output_list[i] < -2:
            x = "Orta derecede bozulma."
        else:
            x = "Ağır bozulma."
        std_dev_verbal_list.append(x)
    return std_dev_verbal_list


def testSbst(patient_ID, patient_age, patient_edu, patient_sex, patient_admin): 
    try:
        while True:
            try:
                print("\n===================================\nSBST: ")
                result_name = ["\nKendiliğinden hatırlama boyutu: ", "\nTanıma boyutu: ", 
                "\nToplam hatırlama boyutu: ",  "\nHatırlama yanlışı boyutu: ", 
                "\nYanlış tanıma boyutu: ", "\nA listesi anlık bellek boyutu: ", 
                "\nA listesi toplam öğrenme boyutu: "]  
                #prints user interface

                sbst_8 = float(input(result_name[0]))
                sbst_10 = float(input(result_name[1]))
                sbst_11 = float(input(result_name[2]))
                sbst_9 = float(input(result_name[3]))
                sbst_12 = float(input(result_name[4]))
                sbst_1 = float(input(result_name[5]))
                sbst_2 = float(input(result_name[6]))
                #gets raw input from the user, these are test results

                result_list = [sbst_8, sbst_10, sbst_11, sbst_9, sbst_12, sbst_1, sbst_2]
                mean_list = []
                sd_list = []
                #creates a list for the test results, and empty ones 
                #for the means and standard deviations
                for i in range(len(result_list)):
                    mean_list.append(result_list[i]-999)
                    sd_list.append(1)            
                    #makes it so that it prints out 999 if there's 
                    #no norm calculated for that group
                norm_exists = True
                #It assumes that there's a norm for every group
                #if none exists, it changes to False

            except:
                print("Lütfen sadece rakam giriniz.")
                testSbst(patient_ID, patient_age, patient_edu, patient_sex, patient_admin)
                continue
                #"Only enter numbers", and then resets the function

            else:
                break

            #tries to get user input, makes sure it's correct input


            """
            Following are lists of means and standard deviations,
            of the specified age, education and sex, where it applies.
            It's in a way that it corresponds to order of the
            result_list.
            """
        if patient_edu <= 7:
            if 15 <= patient_age <= 39:
                mean_list = [13.64, 1.21, 14.86, 0.24, 0.04, 5.50, 116.27]
                sd_list = [1.20, 1.16, 0.35, 0.51, 0.19, 1.64, 10.74]

            elif 40 <= patient_age <= 49:
                mean_list = [13.27, 1.69, 14.97, 0.15, 0.08, 5.32, 109.11]
                sd_list = [1.24, 1.21, 0.18, 0.40, 0.27, 1.35, 11.94]

            elif 50 <= patient_age <= 59:
                mean_list = [13.24, 1.74, 14.98, 0.15, sbst_12, 5.08, 113.50] #Aslında sbst_12 mean = 0.00, SD = 0.00
                sd_list = [1.38, 1.37, 0.12, 0.40, 1, 1.03, 12.00]

            elif 60 <= patient_age <= 69:
                mean_list = [12.90, 2.27, 14.94, 0.20, 0.02, 5.10, 107.80]
                sd_list = [1.36, 2.04, 0.24, 0.46, 0.14, 1.37, 11.16]

            elif 70 <= patient_age <= 79:
                mean_list = [12.93, 2.02, 14.96, 0.24, 0.02, 5.00, 103.69]
                sd_list = [1.44, 1.39, 0.21, 0.43, 0.15, 1.09, 14.21]

            elif 80 <= patient_age:
                mean_list = [10.35, 4.35, 14.70, 0.26, 0.30, 3.96, 88.96]
                sd_list = [1.72, 1.58, 0.64, 0.45, 0.56, 1.66, 14.02]

            else: 
                print("Bu yaş aralığı için norm mevcut değildir.")
                norm_exists = False     
                #"No norm exists for the group", sets it to False

        elif 8 <= patient_edu <= 19:
            if 15 <= patient_age <= 39:
                mean_list = [14.00, 0.97, 14.97, 0.13, sbst_12, 7.87, 131.21] #Aslında sbst_12 mean = 0.00, SD = 0.00
                sd_list = [1.05, 1.04, 0.16, 0.45, 1, 2.05, 10.61]

            elif 40 <= patient_age <= 49:
                mean_list = [13.62, 1.29, 14.91, 0.15, 0.03, 6.98, 126.12]
                sd_list = [1.39, 1.27, 0.29, 0.40, 0.17, 1.96, 11.25]

            elif 50 <= patient_age <= 59:
                mean_list = [13.45, 1.54, 14.97, 0.13, 0.03, 6.61, 124.22]
                sd_list = [1.29, 1.28, 0.17, 0.34, 0.17, 1.50, 10.48]

            elif 60 <= patient_age <= 69:
                mean_list = [13.64, 1.22, 14.88, 0.12, sbst_12, 5.94, 116.88]#Aslında sbst_12 mean = 0.00, SD = 0.00
                sd_list = [1.10, 0.91, 0.39, 0.39, 1, 1.33, 11.15]

            elif 70 <= patient_age <= 79:
                mean_list = [12.29, 2.63, 14.94, 0.13, 0.04, 5.27, 109.75]
                sd_list = [11.88, 1.75, 0.24, 0.34, 0.20, 1.45, 13.98]

            elif 80 <= patient_age:
                mean_list = [11.88, 3.02, 14.89, 0.18, 0.09, 5.19, 109.68]
                sd_list = [1.72, 1.63, 0.36, 0.47, 0.34, 1.64, 16.80]
            else: 
                print("Bu yaş aralığı için norm mevcut değildir.")
                norm_exists = False  
        else:
            print("Bu eğitim grubu için norm mevcut değildir.")
            norm_exists = False  



        std_dev_output_list = calcSd_int(result_list, mean_list, sd_list)
        #it calculated the patient's SD interval as a float using the results, means and the SD
        std_dev_verbal_list = calcSD_more_better(std_dev_output_list)
        #Makes a verbal list to input to the users
        printable_list = outputPrintlist(result_list, std_dev_output_list, std_dev_verbal_list)
        #creates a list to be put into a CSV file

        test_name = 'SBST_data.csv' #test datasının toplanacağı csv dosyasını belirtiyor
        csvWriter(patient_admin, patient_ID, patient_age, patient_sex, patient_edu, test_name, printable_list)
        #writes the printable_list in a CSV file


        console_results = "\n===================================\nSBST testinin sonuçları:"
        for i in range(len(mean_list)):
            console_results = console_results + (result_name[i] + str(outputConsole_results(result_list, std_dev_output_list, std_dev_verbal_list)[i]))            
        console_results = console_results + ("\n===================================\n")
        #prints the list using the range of the list and makes it more appealing for the user (probably should've used a decorator here)

        if norm_exists:
            txtWrite(patient_admin, patient_ID, console_results)
            print(console_results)
            #creates a patient report for the physician and prints it out for the user
        else:
            txtWrite(patient_admin, patient_ID, ("SBST: Bu grup için norm mevcut değildir.\n"+console_results))
            return
            #if there's no norm, it puts "there's no norm for this group" in the patient report

    except:
        print("SBST testini değerlendirirken bir hata oluştu, program kapatılacak.")
        raise
        return
        #saves the program from fiery death

while True:
    mainStartup()

    print("\n===================================\n" +
    "Bilgiler başarıyla girildi, yeniden başlatılıyor.")
#informs the user data has been saved then restarts
\$\endgroup\$
  • \$\begingroup\$ Would you mind including std_dev_int_calc() and which_sd_better_list() so that we have a better idea what this code is doing? \$\endgroup\$ – 200_success Jul 11 '16 at 20:19
  • \$\begingroup\$ Of course, I'll add all the functions the code is referencing at the moment. \$\endgroup\$ – Bilal 'Cheese' Akbulut Jul 11 '16 at 20:20
  • \$\begingroup\$ There we go, I had to translate all my comments, but I put in the bare bones of the whole program in there. The rest of the tests are not there, neither are necessary, I would think. \$\endgroup\$ – Bilal 'Cheese' Akbulut Jul 11 '16 at 20:50
7
\$\begingroup\$

Overview

There's a lot of code to cover here.

The good news is that I was able to understand the code reasonably well, despite the length and the Turkish strings. You did a pretty good job of sticking with English for the code, so it just took a little use of Google Translate to figure out the rest.

If I had to pick the three most important issues, they would be:

  1. Don't misuse functions as goto labels.
  2. Object-oriented programming would be very beneficial here.
  3. You need some better input/output routines, especially for formatting output.

Proper use of functions

Execution starts here:

while True:
    mainStartup()

Then, it asks whether you have entered the admin and patient information correctly ("Yukarıdaki bilgileri onaylıyor musunuz? (e)vet/(h)ayır"). If you say no, then it calls mainStartup() recursively. That is inappropriate nesting; it should be done using a loop instead.

If the information is correct, then it calls mainMenu(). Several places within mainMenu() call mainMenu() recursively, which is similarly inappropriate. Even worse, if the user enters choice 15 to exit, it calls mainStartup(). Since it was the mainStartup() function that had called mainMenu() in the first place, you should really be using return to go back out one level instead of doing mutual recursion.

Your testSbst() function, on the other hand, does it right. When it finishes, it returns control to its caller.

Object-oriented programming

You often pass a cluster of parameters patient_ID, patient_age, patient_edu, patient_sex, and patient_admin. It would be simpler to group those pieces of information together in one Patient object. A namedtuple would be perfect for that.

There are other indications for use of object-oriented programming. The SBST test is one of 15 tests. Those test suites presumably all share some common characteristics. That suggests that SBST should be one of 15 subclasses of a Test base class. Each subclass should know, for example, the name of the test and the various metrics and their reference values. The per-patient test scores, on the other hand, could be considered as instance state in a Test object. You can, eventually, move each test out into its own .py file.

Yet another place where OOP would be useful is in constructing the three types of output. It would be more elegant to define three Reporter classes, which are put in an array reporters = [ScreenReporter, TextFileReporter, CSVReporter]. Then, you can just say...

for reporter in reporters:
    reporter.report(test)

... and each reporter will know how to send the appropriately formatted test results to the screen, the text file, and the CSV file.

Input/ouput routines

Modern Python has

In comparison, manual concatenation is cumbersome:

for i in range(len(std_dev_output_list)):
    console_result.append("Hastanın puanı: " + str(result_list[i]) + ", " + 
    str(std_dev_verbal_list[i]) + ", " + str(std_dev_output_list[i]) + "SD")

You should try to avoid unnecessary string contenation in places like this, too:

print("\n===========================================\n" + 
"Akbulut Standart Sapma Hesaplayıcı v0.4.1" +
"\n===========================================\n")  

Consecutive string literals will automatically be treated as one long string. Better yet, use triple-quoted long strings.

Since a significant part of the work involves asking the user for input and validating that input, it would be worthwhile to add some input/output routines. For example, you have this code that implements a text menu:

def mainMenu(patient_ID, patient_age, patient_edu, patient_sex, patient_admin, menu_done_tests):
    try:
        menu_list = [testStroop, testVf, testSf, testSbst, testTm,
                     testMmt, testCd, testMoca, testEcr, testWisconsin, testWechsler, testRkft, testSdot, testVvt]  
            #list of tests' function names

        test_name_list = ["(1)Stroop ", "(2)Sözel akıcılık ", "(3)Semantik Akıcılık ",
                          "(4)SBST ", "(5)İz sürme ", "(6)Mini Mental test ", "(7)Saat çizme ",
                          "(8)MOCA ", "(9)Artırılmış İpuçlu Hatırlama ", 
                          "(10)Yetişkin Wisconsin Kart Eşleme ", "(11)Wechsler Zeka testi ",
                          "(12)Rey Karmaşık Figür Testi ", "(13)Çizgi yönünü belirleme testi ",
                          "(14)Visual Verbal Test"] 
            #verbal names of the tests

        menu_ui = ("==================================================\n") 
        for i in range(len(test_name_list)):
            if (i+1)%3 == 0:
                menu_ui = menu_ui + test_name_list[i] + "\n"
            else:
                menu_ui = menu_ui + test_name_list[i]                
        menu_ui = menu_ui + "\n"   
        print(menu_ui)
        #creates and prints the mainMenu

        print("Şu ana kadar yapılan testler: ")
        print(menu_done_tests)
        #prints tests that were done so far (could be made into a string, and printed)        

        menu_input = int(input("Girmek istediğiniz testin numarasını giriniz veya çıkış için (" + str(len(menu_list)+1) + ") giriniz: " ))        

If you created support for a generic text menu, then you could reuse it for sex selection:

def inputPatient_sex():   
#a simple loop to get the sex of the patient, provides two options, female or male
    while True:    
        patient_sex_user_input = input("Hastanın cinsiyeti: (1) Kadın - (2) Erkek: ")
        if patient_sex_user_input == "1":
            patient_sex = "Kadın"
            break
        elif patient_sex_user_input == "2":
            patient_sex = "Erkek"
            break
        else:
            print("Lütfen sadece 1 veya 2 giriniz.")
            continue
    return patient_sex

(By the way, you should write function-level comments as a docstrings instead.)

The menu routine, in turn, is a fancy kind prompt for integer input. The menu routine, therefore, should build on the integer-prompting function, which you would need anyway for the age. Integer input, in turn, can build on the string input routine.

When you open() a file using a with block, you don't have to close it. The point of using the with block is that it will close the filehandle for you automatically, no matter how it exits the block.

Data handling

Avoid "stringly typed" data: numbers should be stored as numbers until the very last moment when it needs to be output. These standard deviations, for example, should be calculated, but not formatted as string:

def calcSd_int(result_list, mean_list, sd_list):
    #finds out which SD interval patient result is in and orders it in a list
    std_dev_output_list = []
    for i in range(len(result_list)):
        sd = ((result_list[i] - mean_list[i]) / sd_list[i])
        std_dev_output_list.append(float("%.2f" % sd))
    return std_dev_output_list

Avoid mysterious arrays of non-homogeneous data, whose meaning can only be discerned by looking up the corresponding index somewhere else:

    """
    Following are lists of means and standard deviations,
    of the specified age, education and sex, where it applies.
    It's in a way that it corresponds to order of the
    result_list.
    """
if patient_edu <= 7:
    if 15 <= patient_age <= 39:
        mean_list = [13.64, 1.21, 14.86, 0.24, 0.04, 5.50, 116.27]
        sd_list = [1.20, 1.16, 0.35, 0.51, 0.19, 1.64, 10.74]

    elif 40 <= patient_age <= 49:
        mean_list = [13.27, 1.69, 14.97, 0.15, 0.08, 5.32, 109.11]
        sd_list = [1.24, 1.21, 0.18, 0.40, 0.27, 1.35, 11.94]

    elif 50 <= patient_age <= 59:
        mean_list = [13.24, 1.74, 14.98, 0.15, sbst_12, 5.08, 113.50] #Aslında sbst_12 mean = 0.00, SD = 0.00
        sd_list = [1.38, 1.37, 0.12, 0.40, 1, 1.03, 12.00]

The sbst_12 doesn't make much sense. If you lack reference values for a particular demographic, the sensible value would be None.

Contrast that with the following organization, whose meaning is self-documenting:

class SBST(Test):
    NAME = 'SBST'

    def __init__(self):
        super().__init__(SBST.NAME, metrics=[
            Metric("Kendiliğinden hatırlama boyutu", Metric.MORE_IS_BETTER,
                reference_mean_and_sd=lambda patient:
                    ((None,  None) if patient.age < 15 else
                     (13.64, 1.20) if patient.age < 40 else
                     (13.27, 1.24) if patient.age < 50 else
                     (13.24, 1.38) if patient.age < 60 else
                     (12.90, 1.36) if patient.age < 70 else
                     (12.93, 1.44) if patient.age < 80 else
                     (10.35, 1.72))
                        if patient.edu <  8 else
                    ((None,  None) if patient.age < 15 else
                     (14.00, 1.05) if patient.age < 40 else
                     (13.62, 1.39) if patient.age < 50 else
                     (13.45, 1.29) if patient.age < 60 else
                     (13.64, 1.10) if patient.age < 70 else
                     (12.29, 11.88) if patient.age < 80 else
                     (11.88, 1.72))
                        if patient.edu < 20 else
                    ((None,  None))),

Note the standard deviation of 11.88 for educated septuagenarians — it stands out as a fluke when viewed this way.

I wouldn't worry as much about the verbosity. Once the code works, you can modify it to load the reference values from data files.

Suggested implementation

Input/output classes:

import csv
from collections import namedtuple, OrderedDict
from contextlib import contextmanager
from itertools import chain, groupby
from sys import stdout
from time import strftime
from textwrap import dedent

class TextUI:
    """Input/output routines for a text-based user interface"""
    def print(self, *args):
        print(*args)

    def input(self, prompt=None, cls=str, fail_msg=None):
        while True:
            try:
                return cls(input(prompt))
            except ValueError:
                if fail_msg:
                    self.print(fail_msg)

    def choice(self, prompt, *choices, fail_msg=None):
        while True:
            choice = self.input(prompt)
            for c in choices:
                if choice == c or choice == str(c):
                    return c
                elif hasattr(c, '__iter__') and choice in c:
                    return c[0]
            if fail_msg:
                self.print(fail_msg)

    def bool(self, prompt, fail_msg=None):
        return 'e' == self.choice(prompt, ['e', 'E'], ['h', 'H'], fail_msg)

    def menu(self, choices, prompt=None, fail_msg=None, str=str):
        choices = list(choices)
        s = ''
        for _, row in groupby(enumerate(choices), lambda item: item[0] // 3):
            for i, item in row:
                s += '({}) {}'.format(i + 1, str(item)).ljust(24)
            s += '\n'
        if prompt:
            s += '\n' + prompt
        choice = self.choice(s, *range(1, len(choices) + 1), fail_msg=fail_msg)
        return choices[choice - 1]

class TextFileReporter:
    """A txt file with desired format for the physician"""

    @classmethod
    @contextmanager
    def open(cls, admin, patient):
        date = strftime("%Y-%m-%d")
        file_name = '{}-({})-{}.txt'.format(patient.id, date, admin)
        with open(file_name, 'w', encoding='UTF-8') as file:
            yield cls(admin, patient, file)

    def __init__(self, admin, patient, file):
        self.admin, self.patient, self.file = admin, patient, file
        date = strftime("%Y-%m-%d")
        time = strftime("%H:%M:%S")
        file.write(dedent("""\
        Testi uygulayan: {admin}
        Günün tarihi: {date}
        Saat: {time}
        Hastanın ismi: {patient.name}
        Hastanın kodu: {patient.id}
        Hastanın yaşı: {patient.age}
        Hastanın cinsiyeti: {patient.sex}
        Hastanın toplam eğitim yılı: {patient.edu}
        =============================================
        """)
           .format(admin=admin, date=date, time=time, patient=patient))
        file.flush()

    def report(self, test):
        self.file.write(('=' * 45) + '\n')
        self.file.write("{} testinin sonuçları:\n".format(test.name))
        for metric, value in test.results.items():
            sd = metric.sd_above_mean(self.patient, value)
            sd = 'XXX' if sd is None else '{:.2f}'.format(sd)
            self.file.write("Hastanın puanı: {}, {}, {} SD\n".format(
                metric.name,
                metric.classify(self.patient, value),
                sd
            ))
        self.file.write(('=' * 45) + '\n')
        self.file.flush()

class ScreenReporter(TextFileReporter):
    @classmethod
    @contextmanager
    def open(cls, admin, patient):
        yield cls(admin, patient)

    def __init__(self, admin, patient):
        self.admin, self.patient, self.file = admin, patient, stdout

class CSVReporter:
    @classmethod
    @contextmanager
    def open(cls, admin, patient):
        yield cls(admin, patient)

    def __init__(self, admin, patient):
        self.admin, self.patient = admin, patient

    def report(self, test):
        date = strftime("%Y-%m-%d")
        time = strftime("%H:%M:%S")
        # Note: Legacy encodings such as ISO-8859-9 should be replaced with UTF-8
        with open('{}_data.csv'.format(test.name), 'a', encoding='ISO-8859-9', newline='') as f:
            data_writer = csv.writer(f, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
            result_cols = list(chain(*(
                (metric.name,
                 metric.sd_above_mean(self.patient, value),
                 metric.classify(self.patient, value))
                for metric, value in test.results.items()
            )))
            data_writer.writerow(["",
                self.admin, date, time,
                self.patient.id, self.patient.age, self.patient.sex, self.patient.edu,
            ] + result_cols)

Classes for Patient, Metric, and the base Test:

class Patient(namedtuple('Patient', 'id name age sex edu')):
    @classmethod
    def input(cls, ui):
        id = ui.input("Hastanın kodu: ")
        name = ui.input("Hastanın ismi: ")
        while True:
            age = ui.input(
                "Hastanın yaşı: ",
                int, "Lütfen sadece rakam giriniz."
            )
            if age < 0:
                ui.print("Lütfen 0'dan büyük bir sayı giriniz.")
            else:
                break
        sex = ui.menu(
            ["Kadın", "Erkek"],
            "Hastanın cinsiyeti: ",
            fail_msg="Lütfen sadece 1 veya 2 giriniz."
        )
        edu = ui.input(
            "Hastanın toplam eğitim yılı: ",
            int, "Lütfen sadece rakam giriniz."
        )
        return Patient(id, name, age, sex, edu)

class Metric(namedtuple('Metric', 'name better reference_mean_and_sd')):
    MORE_IS_BETTER = +1
    LESS_IS_BETTER = -1

    def __new__(cls, name, better, reference_mean_and_sd=lambda patient:(None, None)):
        return super().__new__(cls, name, better, reference_mean_and_sd)

    def sd_above_mean(self, patient, value):
        mean, sd = self.reference_mean_and_sd(patient)
        return (value - mean) / sd if sd else None

    def classify(self, patient, value):
        sd = self.sd_above_mean(patient, value)
        if sd is None:
            return "Bu yaş aralığı için norm mevcut değildir"
        elif sd * self.better >= -1:
            return "Normal"
        elif sd * self.better >= -2:
            return "Hafif bozulma"
        elif sd * self.better >= -3:
            return "Orta derecede bozulma"
        else:
            return "Ağır bozulma"

class Test:
    def __init__(self, name, metrics):
        self.name = name
        self.metrics = metrics
        self.results = OrderedDict()

    def __str__(self):
        return self.name

    def input(self, ui, patient):
        print('{}\n{}: '.format('=' * 45, self.name))
        for metric in self.metrics:
            self.results[metric] = ui.input(metric.name + ': ', float)

The SBST test:

class SBST(Test):
    NAME = 'SBST'

    def __init__(self):
        super().__init__(SBST.NAME, metrics=[
            Metric("Kendiliğinden hatırlama boyutu", Metric.MORE_IS_BETTER,
                reference_mean_and_sd=lambda patient:
                    ((None,  None) if patient.age < 15 else
                     (13.64, 1.20) if patient.age < 40 else
                     (13.27, 1.24) if patient.age < 50 else
                     (13.24, 1.38) if patient.age < 60 else
                     (12.90, 1.36) if patient.age < 70 else
                     (12.93, 1.44) if patient.age < 80 else
                     (10.35, 1.72))
                        if patient.edu <  8 else
                    ((None,  None) if patient.age < 15 else
                     (14.00, 1.05) if patient.age < 40 else
                     (13.62, 1.39) if patient.age < 50 else
                     (13.45, 1.29) if patient.age < 60 else
                     (13.64, 1.10) if patient.age < 70 else
                     (12.29, 11.88) if patient.age < 80 else
                     (11.88, 1.72))
                        if patient.edu < 20 else
                    ((None,  None))),
            Metric("Tanıma boyutu", Metric.MORE_IS_BETTER,
                reference_mean_and_sd=lambda patient:
                    ((None, None) if patient.age < 15 else
                     (1.21, 1.16) if patient.age < 40 else
                     (1.69, 1.21) if patient.age < 50 else
                     (1.74, 1.37) if patient.age < 60 else
                     (2.27, 2.04) if patient.age < 70 else
                     (2.02, 1.39) if patient.age < 80 else
                     (4.35, 1.58))
                        if patient.edu <  8 else
                    ((None, None) if patient.age < 15 else
                     (0.97, 1.04) if patient.age < 40 else
                     (1.29, 1.27) if patient.age < 50 else
                     (1.54, 1.28) if patient.age < 60 else
                     (1.22, 0.91) if patient.age < 70 else
                     (2.63, 1.75) if patient.age < 80 else
                     (3.92, 1.63))
                        if patient.edu < 20 else
                    ((None,  None))),
            Metric("Toplam hatırlama boyutu", Metric.MORE_IS_BETTER), # TODO
            Metric("Hatırlama yanlışı boyutu", Metric.MORE_IS_BETTER),
            Metric("Yanlış tanıma boyutu", Metric.MORE_IS_BETTER),
            Metric("A listesi anlık bellek boyutu", Metric.MORE_IS_BETTER),
            Metric("A listesi toplam öğrenme boyutu", Metric.MORE_IS_BETTER),
        ])

Putting it all together:

# TODO: Implement more types of tests
TESTS = [
    SBST, SBST, SBST,
    SBST, SBST, 
]

def select_test(ui, tests_done):
    exit_cmd = namedtuple('ExitCommand', 'NAME')("çıkış")
    choices = TESTS + [exit_cmd]
    while True:
        prompt = "Girmek istediğiniz testin numarasını giriniz: "
        if tests_done:
            prompt = ("Şu ana kadar yapılan testler: \n" +
                      (', '.join(test.name for test in tests_done)) +
                      '\n\n' +
                      prompt)
        choice = ui.menu(
            choices,
            prompt,
            fail_msg="Lütfen listede olan numaralardan giriniz.",
            str=lambda choice:choice.NAME
        )
        if choice == exit_cmd:
            return
        yield choice()

def main(ui):
    while True:
        print(dedent("""\
        ===========================================
        Akbulut Standart Sapma Hesaplayıcı v0.4.1
        ===========================================
        """))
        # The person who administrated the test
        admin = ui.input("Testi uygulayan kişi: ")
        patient = Patient.input(ui)
        if ui.bool("Yukarıdaki bilgileri onaylıyor musunuz? (e)vet/(h)ayır: "):
            break
        ui.print("Program tekrar başlatılıyor.")

    with ScreenReporter.open(admin, patient) as report1, \
         TextFileReporter.open(admin, patient) as report2, \
         CSVReporter.open(admin, patient) as report3:
        reporters = [report1, report2, report3]
        tests_done = []
        for test in select_test(ui, tests_done):
            if test.__class__ in (t.__class__ for t in tests_done):
                # Ask the user if he administered the test before and continues
                # only if approved,
                if not ui.bool("Bu test zaten girilmiş, tekrar girmek istiyor musunuz? (e)vet/(h)ayır:"):
                    ui.print("Bir önceki basamağa geri dönülüyor.")
                    continue
                # This creates a duplicate results in the files and prompts the
                # user to remove it manually later
                ui.print("Bu test daha öncesinde yapıldığı için, veri bankasında birden fazla girişe"
                      " neden olacaktır, lütfen sonrasında düzeltmeyi unutmayınız.")
            test.input(ui, patient)
            for reporter in reporters:
                reporter.report(test)
            tests_done.append(test)

if __name__ == '__main__':
    while True:
        main(TextUI())

Prompting the user to manually remove duplicate results from the output seems like a deficiency of the code. You could avoid that by postponing the output until "Exit" is selected, and deduplicating tests_done before calling the output routines.

\$\endgroup\$
  • \$\begingroup\$ Wow. Thanks, thanks a lot. This is way more than I expected when I put this up here. --- For the code, well, like any newbie, I'm struggling to grasp the concept of classes, at least to the extend to use them comfortably. On top of that, this is my first project and I went with the easiest, dirtiest solution. I probably should've persevered in trying to use OOP. --- But studying all the things you've wrote, I'VE SEEN THE ERROR OF MY WAYS. Thank you, once again. I'll implement things the way you suggested where I can and figure it out for the slightly different test formats. \$\endgroup\$ – Bilal 'Cheese' Akbulut Jul 15 '16 at 16:11

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.