# Calculate where the patient results reside within the normal distribution

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()

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:
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":
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:
continue
else:
break
return patient_edu

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

for i in range(len(test_name_list)):
if (i+1)%3 == 0:
else:

print("Şu ana kadar yapılan testler: ")
#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
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.")
#returns to the previous step

#using the number, calls the function user wanted, adds that to the done tests
print("\nİşlem iptal edildi, şu ana kadar yapılanlar kaydedildi. " +
"Program baştan başlatılıyor.")
mainStartup()
#this is the exit command

print("\nLütfen listede olan numaralardan giriniz.")
#this is called if user enters a number higher than tests that are in the program
except:
#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
#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":
#creates a txt file for future data entry
#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:
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:
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

• 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? Commented Jul 11, 2016 at 20:19
• Of course, I'll add all the functions the code is referencing at the moment. Commented Jul 11, 2016 at 20:20
• 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. Commented Jul 11, 2016 at 20:50

## 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

for i in range(len(test_name_list)):
if (i+1)%3 == 0:
else:

print("Şu ana kadar yapılan testler: ")
#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":
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
date = strftime("%Y-%m-%d")
with open(file_name, 'w', encoding='UTF-8') as file:

date = strftime("%Y-%m-%d")
time = strftime("%H:%M:%S")
file.write(dedent("""\
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}
=============================================
""")
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

class CSVReporter:
@classmethod
@contextmanager

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.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şı: ",
)
if age < 0:
ui.print("Lütfen 0'dan büyük bir sayı giriniz.")
else:
break
"Hastanın cinsiyeti: ",
fail_msg="Lütfen sadece 1 veya 2 giriniz."
)
edu = ui.input(
"Hastanın toplam eğitim yılı: ",
)
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)
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, \
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.

• 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. Commented Jul 15, 2016 at 16:11