text: import json
  import pathlib
  import numpy as np
  import matplotlib.pyplot as plt
  import re
  import pandas as pd
  import os.path
  from os import path
  import math
  
  class QuestionnaireAnalysis:
      """
      Reads and analyzes data generated by the questionnaire experiment.
      Should be able to accept strings and pathlib.Path objects.
      """
  
      def __init__(self, data_fname):
          self.data_fname=pathlib.WindowsPath(data_fname)
          if path.exists(data_fname)==False:
             raise ValueError('there is no such file')
  
      def read_data(self):
          """Reads the json data located in self.data_fname into memory, to
          the attribute self.data.
          """
          with open(self.data_fname) as f:
              data = json.loads(f.read())
          self.data=pd.DataFrame(data)
          return (self.data)
  
      def show_age_distrib(self) :
          """Calculates and plots the age distribution of the participants.
  
          Returns
          -------
          hist : np.ndarray
          Number of people in a given bin
          bins : np.ndarray
          Bin edges
              """
          data=QuestionnaireAnalysis.read_data(self)
          df_json=pd.DataFrame(data)
          bins=[0,10,20,30,40,50,60,70,80,90,100]
          hist=plt.hist(df_json.loc[df_json['age']!="nan",'age'],bins=bins)[0]
          bins=plt.hist(df_json.loc[df_json['age']!="nan",'age'],bins=bins)[1]
          plt.ylabel('Number of participants')
          plt.xlabel('Age')
  
          return((hist,bins))
  
      def remove_rows_without_mail(self):
          """Checks self.data for rows with invalid emails, and removes them.
  
          Returns
          -------
          df : pd.DataFrame
          A corrected DataFrame, i.e. the same table but with the erroneous rows removed and
          the (ordinal) index after a reset. 
              """
          data=QuestionnaireAnalysis.read_data(self)
          df_json=pd.DataFrame(data)
          mask=[]
          for ind in range(len(df_json)):
              if re.search(r'\w+@\w+.c',df_json.iloc[ind,3]):
                  mask.append(ind)
          correct_email_df=df_json.iloc[mask]
          correct_email_df.index=list(range(len(correct_email_df)))
          return(correct_email_df)
  
      def fill_na_with_mean(self):
          """Finds, in the original DataFrame, the subjects that didn't answer
          all questions, and replaces that missing value with the mean of the
          other grades for that student.
  
          Returns
          -------
          df : pd.DataFrame
          The corrected DataFrame after insertion of the mean grade
          arr : np.ndarray
          Row indices of the students that their new grades were generated
          """
          data=QuestionnaireAnalysis.read_data(self)
          df_json=pd.DataFrame(data)
          arr=[]
          log_mask=df_json.iloc[:,7:12]=='nan'
          for row in range(len(log_mask)):
              if log_mask.iloc[row,:].any():
                  arr.append(row)
                  sub_mean=np.mean(df_json.iloc[row,7:12]!='nan')
                  df_json.update(df_json.iloc[row:row+1,7:12].where(df_json!='nan',other=sub_mean))
          arr=np.asarray(arr,dtype='int64')
          json_out=df_json.to_json(orient='records')
          with open(self.data_fname, 'w') as f:
              f.write(json_out)
          return((df_json,arr))
  
      def score_subjects(self, maximal_nans_per_sub: int = 1) -> pd.DataFrame:
          """Calculates the average score of a subject and adds a new "score" column
          with it.
  
          If the subject has more than "maximal_nans_per_sub" NaN in his grades, the
          score should be NA. Otherwise, the score is simply the mean of the other grades.
          The datatype of score is UInt8, and the floating point raw numbers should be
          rounded down.
  
          Parameters
          ----------
          maximal_nans_per_sub : int, optional
          Number of allowed NaNs per subject before giving a NA score.
  
          Returns
          -------
          pd.DataFrame
          A new DF with a new column - "score".
          """
          data=QuestionnaireAnalysis.read_data(self)
          df_json=pd.DataFrame(data)
  
          scores=df_json.iloc[:,7:12].where(df_json!='nan')
          count_not_nan=scores.count(axis='columns')
          final_scores=[]
          for subject in range(len(df_json)):
              if count_not_nan.iloc[subject]<5-maximal_nans_per_sub:
                  final_scores.append(np.nan)
              else:
                  final_scores.append(math.floor(np.mean(scores.iloc[subject,:])))
          df_final_scores=pd.DataFrame(final_scores,columns=['score'])
          df_final_scores=df_final_scores.astype(pd.UInt8Dtype())
          df_end=pd.concat([df_json,df_final_scores],axis=1)
  
          return(df_end)
          
  
  
  
  