code: import pandas as pd
  import math
  import numpy as np
  import time
  import imp
  import pickle
  import os
  from os import path
  import json
  
  from chefboost.commons import functions, evaluate as eval
  from chefboost.training import Preprocess, Training
  from chefboost.tuning import gbm, adaboost, randomforest
  
  #------------------------
  
  def fit(df, config = {}, target_label = 'Decision', validation_df = None):
  
  	"""
  	Parameters:
  		df (pandas data frame): Training data frame. The target column must be named as 'Decision' and it has to be in the last column
  
  		config (dictionary):
  
  			config = {
  				'algorithm' (string): ID3, 'C4.5, CART, CHAID or Regression
  				'enableParallelism' (boolean): False
  
  				'enableGBM' (boolean): True,
  				'epochs' (int): 7,
  				'learning_rate' (int): 1,
  
  				'enableRandomForest' (boolean): True,
  				'num_of_trees' (int): 5,
  
  				'enableAdaboost' (boolean): True,
  				'num_of_weak_classifier' (int): 4
  			}
  
  		validation_df (pandas data frame): if nothing is passed to validation data frame, then the function validates built trees for training data frame
  
  	Returns:
  		chefboost model
  
  	"""
  
  	#------------------------
  
  	process_id = os.getpid()
  
  	#------------------------
  	#rename target column name
  	if target_label != 'Decision':
  		df = df.rename(columns = {target_label: 'Decision'})
  
  	#if target is not the last column
  	if df.columns[-1] != 'Decision':
  		if 'Decision' in df.columns:
  			new_column_order = df.columns.drop('Decision').tolist() + ['Decision']
  			#print(new_column_order)
  			df = df[new_column_order]
  		else:
  			raise ValueError('Please set the target_label')
  
  	#------------------------
  
  	base_df = df.copy()
  
  	#------------------------
  
  	target_label = df.columns[len(df.columns)-1]
  	if target_label != 'Decision':
  		print("Expected: Decision, Existing: ",target_label)
  		raise ValueError('Please confirm that name of the target column is "Decision" and it is put to the right in pandas data frame')
  
  	#------------------------
  	#handle NaN values
  
  	nan_values = []
  
  	for column in df.columns:
  		if df[column].dtypes != 'object':
  			min_value = df[column].min()
  			idx = df[df[column].isna()].index
  
  			nan_value = []
  			nan_value.append(column)
  
  			if idx.shape[0] > 0:
  				df.loc[idx, column] = min_value - 1
  				nan_value.append(min_value - 1)
  				min_value - 1
  				#print("NaN values are replaced to ", min_value - 1, " in column ", column)
  			else:
  				nan_value.append(None)
  
  			nan_values.append(nan_value)
  
  	#------------------------
  
  	#initialize params and folders
  	config = functions.initializeParams(config)
  	functions.initializeFolders()
  
  	#------------------------
  
  	algorithm = config['algorithm']
  
  	valid_algorithms = ['ID3', 'C4.5', 'CART', 'CHAID', 'Regression']
  
  	if algorithm not in valid_algorithms:
  		raise ValueError('Invalid algorithm passed. You passed ', algorithm," but valid algorithms are ",valid_algorithms)
  
  	#------------------------
  
  	enableRandomForest = config['enableRandomForest']
  	num_of_trees = config['num_of_trees']
  	enableMultitasking = config['enableMultitasking'] #no longer used. check to remove this variable.
  
  	enableGBM = config['enableGBM']
  	epochs = config['epochs']
  	learning_rate = config['learning_rate']
  
  	enableAdaboost = config['enableAdaboost']
  	enableParallelism = config['enableParallelism']
  
  	#------------------------
  
  	if enableParallelism == True:
  		print("[INFO]: ",config["num_cores"],"CPU cores will be allocated in parallel running")
  
  		from multiprocessing import set_start_method, freeze_support
  		set_start_method("spawn", force=True)
  		freeze_support()
  	#------------------------
  	raw_df = df.copy()
  	num_of_rows = df.shape[0]; num_of_columns = df.shape[1]
  
  	if algorithm == 'Regression':
  		if df['Decision'].dtypes == 'object':
  			raise ValueError('Regression trees cannot be applied for nominal target values! You can either change the algorithm or data set.')
  
  	if df['Decision'].dtypes != 'object': #this must be regression tree even if it is not mentioned in algorithm
  
  		if algorithm != 'Regression':
  			print("WARNING: You set the algorithm to ", algorithm," but the Decision column of your data set has non-object type.")
  			print("That's why, the algorithm is set to Regression to handle the data set.")
  
  		algorithm = 'Regression'
  		config['algorithm'] = 'Regression'
  		global_stdev = df['Decision'].std(ddof=0)
  
  	if enableGBM == True:
  		print("Gradient Boosting Machines...")
  		algorithm = 'Regression'
  		config['algorithm'] = 'Regression'
  
  	if enableAdaboost == True:
  		#enableParallelism = False
  		for j in range(0, num_of_columns):
  			column_name = df.columns[j]
  			if df[column_name].dtypes  == 'object':
  				raise ValueError('Adaboost must be run on numeric data set for both features and target')
  
  	#-------------------------
  
  	print(algorithm," tree is going to be built...")
  
  	dataset_features = dict() #initialize a dictionary. this is going to be used to check features numeric or nominal. numeric features should be transformed to nominal values based on scales.
  
  	header = "def findDecision(obj): #"
  
  	num_of_columns = df.shape[1]-1
  	for i in range(0, num_of_columns):
  		column_name = df.columns[i]
  		dataset_features[column_name] = df[column_name].dtypes
  		header = header + "obj[" + str(i) +"]: "+column_name
  		if i != num_of_columns - 1:
  			header = header + ", "
  
  	header = header + "\n"
  
  	#------------------------
  
  	begin = time.time()
  
  	trees = []; alphas = []
  
  	if enableAdaboost == True:
  		trees, alphas = adaboost.apply(df, config, header, dataset_features, validation_df = validation_df, process_id = process_id)
  
  	elif enableGBM == True:
  
  		if df['Decision'].dtypes == 'object': #transform classification problem to regression
  			trees, alphas = gbm.classifier(df, config, header, dataset_features, validation_df = validation_df, process_id = process_id)
  			classification = True
  
  		else: #regression
  			trees = gbm.regressor(df, config, header, dataset_features, validation_df = validation_df, process_id = process_id)
  			classification = False
  
  	elif enableRandomForest == True:
  		trees = randomforest.apply(df, config, header, dataset_features, validation_df = validation_df, process_id = process_id)
  	else: #regular decision tree building
  
  		root = 1; file = "outputs/rules/rules.py"
  		functions.createFile(file, header)
  
  		if enableParallelism == True:
  			json_file = "outputs/rules/rules.json"
  			functions.createFile(json_file, "[\n")
  
  		trees = Training.buildDecisionTree(df, root = root, file = file, config = config
  				, dataset_features = dataset_features
  				, parent_level = 0, leaf_id = 0, parents = 'root', validation_df = validation_df, main_process_id = process_id)
  
  	print("-------------------------")
  	print("finished in ",time.time() - begin," seconds")
  
  	obj = {
  		"trees": trees,
  		"alphas": alphas,
  		"config": config,
  		"nan_values": nan_values
  	}
  
  	#-----------------------------------------
  
  	#train set accuracy
  	df = base_df.copy()
  	evaluate(obj, df, task = 'train')
  
  	#validation set accuracy
  	if isinstance(validation_df, pd.DataFrame):
  		evaluate(obj, validation_df, task = 'validation')
  
  	#-----------------------------------------
  
  	return obj
  
  	#-----------------------------------------
  
  def predict(model, param):
  
  	"""
  	Parameters:
  		model (built chefboost model): you should pass model argument to the return of fit function
  		param (list): pass input features as python list
  
  		e.g. chef.predict(model, param = ['Sunny', 'Hot', 'High', 'Weak'])
  	Returns:
  		prediction
  	"""
  
  	trees = model["trees"]
  	config = model["config"]
  
  	alphas = []
  	if "alphas" in model:
  		alphas = model["alphas"]
  
  	nan_values = []
  	if "nan_values" in model:
  		nan_values = model["nan_values"]
  
  	#-----------------------
  	#handle missing values
  
  	column_index = 0
  	for column in nan_values:
  		column_name = column[0]
  		missing_value = column[1]
  
  		if pd.isna(missing_value) != True:
  			#print("missing values will be replaced with ",missing_value," in ",column_name," column")
  
  			if pd.isna(param[column_index]):
  				param[column_index] = missing_value
  
  		column_index = column_index + 1
  
  	#print("instance: ", param)
  	#-----------------------
  
  	enableGBM = config['enableGBM']
  	adaboost = config['enableAdaboost']
  	enableRandomForest = config['enableRandomForest']
  
  	#-----------------------
  
  	classification = False
  	prediction = 0
  	prediction_classes = []
  
  	#-----------------------
  
  	if enableGBM == True:
  
  		if len(trees) == config['epochs']:
  			classification = False
  		else:
  			classification = True
  			prediction_classes = [0 for i in alphas]
  
  	#-----------------------
  
  	if len(trees) > 1: #bagging or boosting
  		index = 0
  		for tree in trees:
  			if adaboost != True:
  
  				custom_prediction = tree.findDecision(param)
  
  				if custom_prediction != None:
  					if type(custom_prediction) != str: #regression
  
  						if enableGBM == True and classification == True:
  							prediction_classes[index % len(alphas)] += custom_prediction
  						else:
  							prediction += custom_prediction
  					else:
  						classification = True
  						prediction_classes.append(custom_prediction)
  			else: #adaboost
  				prediction += alphas[index] * tree.findDecision(param)
  			index = index + 1
  
  		if enableRandomForest == True:
  			#notice that gbm requires cumilative sum but random forest requires mean of each tree
  			prediction = prediction / len(trees)
  
  		if adaboost == True:
  			prediction = functions.sign(prediction)
  	else: #regular decision tree
  		tree = trees[0]
  		prediction = tree.findDecision(param)
  
  	if classification == False:
  		return prediction
  	else:
  		if enableGBM == True and classification == True:
  			return alphas[np.argmax(prediction_classes)]
  		else: #classification
  			#e.g. random forest
  			#get predictions made by different trees
  			predictions = np.array(prediction_classes)
  
  			#find the most frequent prediction
  			(values, counts) = np.unique(predictions, return_counts=True)
  			idx = np.argmax(counts)
  			prediction = values[idx]
  
  			return prediction
  
  def save_model(base_model, file_name="model.pkl"):
  
  	"""
  	Parameters:
  		base_model (built chefboost model): you should pass this to the return of fit function
  		file_name (string): you should pass target file name as exact path.
  	"""
  
  	model = base_model.copy()
  
  	#modules cannot be saved. Save its reference instead.
  	module_names = []
  	for tree in model["trees"]:
  		module_names.append(tree.__name__)
  
  	model["trees"] = module_names
  
  	f = open("outputs/rules/"+file_name, "wb")
  	pickle.dump(model,f)
  	f.close()
  
  def load_model(file_name="model.pkl"):
  
  	"""
  	Parameters:
  		file_name (string): exact path of the target saved model
  	Returns:
  		built chefboost model
  	"""
  
  	f = open('outputs/rules/'+file_name, 'rb')
  	model = pickle.load(f)
  
  	#restore modules from its references
  	modules = []
  	for model_name in model["trees"]:
  		module = functions.restoreTree(model_name)
  		modules.append(module)
  
  	model["trees"] = modules
  
  	return model
  
  def restoreTree(moduleName):
  
  	"""
  	If you have decision rules, then this function enables you to load a built chefboost model. You can then call prediction.
  	Parameters:
  		moduleName (string): you should pass outputs/rules/rules if you want to restore outputs/rules/rules.py
  
  	Returns:
  		built chefboost model
  	"""
  
  	return functions.restoreTree(moduleName)
  
  def feature_importance(rules):
  
  	"""
  	Parameters:
  		rules (string or list):
  
  		e.g. decision_rules = "outputs/rules/rules.py"
  		or this could be retrieved from built model as shown below.
  
  			decision_rules = []
  			for tree in model["trees"]:
  			   rule = .__dict__["__spec__"].origin
  			   decision_rules.append(rule)
  
  	Returns:
  		pandas data frame
  	"""
  
  	if type(rules) != list:
  		rules = [rules]
  	else:
  		print("rules: ",rules)
  
  	#-----------------------------
  
  	dfs = []
  
  	for rule in rules:
  		print("Decision rule: ",rule)
  
  		file = open(rule, 'r')
  		lines = file.readlines()
  
  		pivot = {}
  		rules = []
  
  		#initialize feature importances
  		line_idx = 0
  		for line in lines:
  			if line_idx == 0:
  				feature_explainer_list = line.split("#")[1].split(", ")
  				for feature_explainer in feature_explainer_list:
  					feature = feature_explainer.split(": ")[1].replace("\n", "")
  					pivot[feature] = 0
  			else:
  				if "# " in line:
  					rule = line.strip().split("# ")[1]
  					rules.append(json.loads(rule))
  
  			line_idx = line_idx + 1
  
  		feature_names = list(pivot.keys())
  
  		for feature in feature_names:
  			for rule in rules:
  				if rule["feature"] == feature:
  
  
  					score = rule["metric_value"] * rule["instances"]
  					current_depth = rule["depth"]
  
  					child_scores = 0
  					#find child node importances
  					for child_rule in rules:
  						if child_rule["depth"] == current_depth + 1:
  
  							child_score = child_rule["metric_value"] * child_rule["instances"]
  
  							child_scores = child_scores + child_score
  
  					score = score - child_scores
  
  					pivot[feature] = pivot[feature] + score
  
  		#normalize feature importance
  
  		total_score = 0
  		for feature, score in pivot.items():
  			total_score = total_score + score
  
  		for feature, score in pivot.items():
  			pivot[feature] = round(pivot[feature] / total_score, 4)
  
  		instances = []
  		for feature, score in pivot.items():
  			instance = []
  			instance.append(feature)
  			instance.append(score)
  			instances.append(instance)
  
  		df = pd.DataFrame(instances, columns = ["feature", "final_importance"])
  		df = df.sort_values(by = ["final_importance"], ascending = False)
  
  		if len(rules) == 1:
  			return df
  		else:
  			dfs.append(df)
  
  	if len(rules) > 1:
  
  		hf = pd.DataFrame(feature_names, columns = ["feature"])
  		hf["importance"] = 0
  
  		for df in dfs:
  			hf = hf.merge(df, on = ["feature"], how = "left")
  			hf["importance"] = hf["importance"] + hf["final_importance"]
  			hf = hf.drop(columns = ["final_importance"])
  
  		#------------------------
  		#normalize
  		hf["importance"] = hf["importance"] / hf["importance"].sum()
  		hf = hf.sort_values(by = ["importance"], ascending = False)
  
  		return hf
  
  def evaluate(model, df, target_label = 'Decision', task = 'test'):
  
  	"""
  	Parameters:
  		model (built chefboost model): you should pass the return of fit function
  		df (pandas data frame): data frame you would like to evaluate
  		task (string): optionally you can pass this train, validation or test
  	"""
  
  	#--------------------------
  
  	if target_label != 'Decision':
  		df = df.rename(columns = {target_label: 'Decision'})
  
  	#if target is not the last column
  	if df.columns[-1] != 'Decision':
  		new_column_order = df.columns.drop('Decision').tolist() + ['Decision']
  		print(new_column_order)
  		df = df[new_column_order]
  
  	#--------------------------
  
  	functions.bulk_prediction(df, model)
  
  	enableAdaboost = model["config"]["enableAdaboost"]
  
  	if enableAdaboost == True:
  		df['Decision'] = df['Decision'].astype(str)
  		df['Prediction'] = df['Prediction'].astype(str)
  
  	eval.evaluate(df, task = task)
  
apis: pandas.DataFrame pandas.isna numpy.argmax numpy.array numpy.unique