In the last notebooks the best parameters for the datasets (base period, training period) were found, for some models, and for the prediction of 1, 7, 14, 28 and 56 days ahead. Two models were finally considered: Linear Regression and Random Forest. In this notebook the hyperparameters will be tuned for the Random Forest, and the best model will be chosen for each "ahead day" (1, 7, 14, 28, 56).


In [53]:
# Basic imports
import os
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import datetime as dt
import scipy.optimize as spo
import sys
from time import time
from sklearn.metrics import r2_score, median_absolute_error

%matplotlib inline

%pylab inline
pylab.rcParams['figure.figsize'] = (20.0, 10.0)

%load_ext autoreload
%autoreload 2

sys.path.append('../../')
import utils.misc as misc


Populating the interactive namespace from numpy and matplotlib
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

Let's first organize the results from the previous notebooks


In [18]:
res_df = pd.read_pickle('../../data/results_ahead1_linear_df.pkl')

In [19]:
res_df.head()


Out[19]:
base_days ahead_days train_val_time step_days GOOD_DATA_RATIO SAMPLES_GOOD_DATA_RATIO x_filename y_filename train_days scores r2 mre
0 7 1 -1 7 0.99 0.9 x_base7_ahead1.pkl y_base7_ahead1.pkl 252 (0.834741188062, 0.0153118291225) 0.834741 0.015312
1 14 1 -1 7 0.99 0.9 x_base14_ahead1.pkl y_base14_ahead1.pkl 252 (0.900241572634, 0.0167286326766) 0.900242 0.016729
2 28 1 -1 7 0.99 0.9 x_base28_ahead1.pkl y_base28_ahead1.pkl 252 (0.952896891193, 0.015095253567) 0.952897 0.015095
3 56 1 -1 7 0.99 0.9 x_base56_ahead1.pkl y_base56_ahead1.pkl 252 (0.974173264084, 0.0156013450074) 0.974173 0.015601
4 112 1 -1 7 0.99 0.9 x_base112_ahead1.pkl y_base112_ahead1.pkl 252 (0.985143962052, 0.0170847165167) 0.985144 0.017085

In [37]:
RELEVANT_COLUMNS = ['base_days', 
                    'train_days', 
                    'r2',
                    'mre',
                    'ahead_days',
                    'train_val_time',
                    'step_days',
                    'GOOD_DATA_RATIO',
                    'SAMPLES_GOOD_DATA_RATIO',
                    'x_filename',
                    'y_filename']

best_params_df = res_df[RELEVANT_COLUMNS].loc[np.argmin(res_df['mre']),:]
best_params_df['model'] = 'linear'
best_params_df


Out[37]:
base_days                                    56
train_days                                  504
r2                                     0.260404
mre                                    0.129533
ahead_days                                   56
train_val_time                               -1
step_days                                     7
GOOD_DATA_RATIO                            0.99
SAMPLES_GOOD_DATA_RATIO                     0.9
x_filename                 x_base56_ahead56.pkl
y_filename                 y_base56_ahead56.pkl
model                                    linear
Name: 8, dtype: object

In [38]:
test_df = pd.DataFrame()
test_df.append(best_params_df, ignore_index=True)


Out[38]:
GOOD_DATA_RATIO SAMPLES_GOOD_DATA_RATIO ahead_days base_days model mre r2 step_days train_days train_val_time x_filename y_filename
0 0.99 0.9 56.0 56.0 linear 0.129533 0.260404 7.0 504.0 -1.0 x_base56_ahead56.pkl y_base56_ahead56.pkl

In [39]:
RELEVANT_COLUMNS = ['base_days', 
                    'train_days', 
                    'r2',
                    'mre',
                    'ahead_days',
                    'train_val_time',
                    'step_days',
                    'GOOD_DATA_RATIO',
                    'SAMPLES_GOOD_DATA_RATIO',
                    'x_filename',
                    'y_filename']

ahead_days_list = [1, 7, 14, 28, 56]
models_list = ['linear', 'random_forest']

results_df = pd.DataFrame()
for ahead_days in ahead_days_list:
    for model in models_list:
        res_df = pd.read_pickle('../../data/results_ahead{}_{}_df.pkl'.format(ahead_days, model))
        best_params_df = res_df[RELEVANT_COLUMNS].loc[np.argmax(res_df['r2']),:]
        best_params_df['ahead_days'] = ahead_days
        best_params_df['model'] = model
        results_df = results_df.append(best_params_df, ignore_index=True)

In [40]:
results_df


Out[40]:
GOOD_DATA_RATIO SAMPLES_GOOD_DATA_RATIO ahead_days base_days model mre r2 step_days train_days train_val_time x_filename y_filename
0 0.99 0.9 1.0 112.0 linear 0.015856 0.986599 7.0 504.0 -1.0 x_base112_ahead1.pkl y_base112_ahead1.pkl
1 0.99 0.9 1.0 112.0 random_forest 0.018002 0.984864 7.0 756.0 -1.0 x_base112_ahead1.pkl y_base112_ahead1.pkl
2 0.99 0.9 7.0 112.0 linear 0.042367 0.923348 7.0 756.0 -1.0 x_base112_ahead7.pkl y_base112_ahead7.pkl
3 0.99 0.9 7.0 112.0 random_forest 0.044267 0.915048 7.0 756.0 -1.0 x_base112_ahead7.pkl y_base112_ahead7.pkl
4 0.99 0.9 14.0 112.0 linear 0.060167 0.865259 7.0 756.0 -1.0 x_base112_ahead14.pkl y_base112_ahead14.pkl
5 0.99 0.9 14.0 112.0 random_forest 0.063327 0.829452 7.0 756.0 -1.0 x_base112_ahead14.pkl y_base112_ahead14.pkl
6 0.99 0.9 28.0 112.0 linear 0.091966 0.758046 7.0 756.0 -1.0 x_base112_ahead28.pkl y_base112_ahead28.pkl
7 0.99 0.9 28.0 112.0 random_forest 0.096087 0.715802 7.0 756.0 -1.0 x_base112_ahead28.pkl y_base112_ahead28.pkl
8 0.99 0.9 56.0 112.0 linear 0.127913 0.590426 7.0 756.0 -1.0 x_base112_ahead56.pkl y_base112_ahead56.pkl
9 0.99 0.9 56.0 112.0 random_forest 0.136095 0.512861 7.0 756.0 -1.0 x_base112_ahead56.pkl y_base112_ahead56.pkl

In [41]:
results_df.to_pickle('../../data/best_dataset_params_raw_df.pkl')

Which is the best model before hyperparameter tuning?


In [42]:
def keep_max_r2(record):
    return record.loc[np.argmax(record['r2']),:]

best_r2_df = results_df.groupby('ahead_days').apply(keep_max_r2)
best_r2_df


Out[42]:
GOOD_DATA_RATIO SAMPLES_GOOD_DATA_RATIO ahead_days base_days model mre r2 step_days train_days train_val_time x_filename y_filename
ahead_days
1.0 0.99 0.9 1.0 112.0 linear 0.015856 0.986599 7.0 504.0 -1.0 x_base112_ahead1.pkl y_base112_ahead1.pkl
7.0 0.99 0.9 7.0 112.0 linear 0.042367 0.923348 7.0 756.0 -1.0 x_base112_ahead7.pkl y_base112_ahead7.pkl
14.0 0.99 0.9 14.0 112.0 linear 0.060167 0.865259 7.0 756.0 -1.0 x_base112_ahead14.pkl y_base112_ahead14.pkl
28.0 0.99 0.9 28.0 112.0 linear 0.091966 0.758046 7.0 756.0 -1.0 x_base112_ahead28.pkl y_base112_ahead28.pkl
56.0 0.99 0.9 56.0 112.0 linear 0.127913 0.590426 7.0 756.0 -1.0 x_base112_ahead56.pkl y_base112_ahead56.pkl

In [43]:
best_r2_df[['mre', 'r2']].plot()


Out[43]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f1c4001dba8>

Before hyperparameter tuning, it seems like the linear regression is doing better in all the predictions. Clearly, as the days ahead are more, the r2 value drops, and the mre goes up.

Let's search for better hyperparameters for the Random Forest models


In [44]:
initial_performance_df = results_df[results_df['model']=='random_forest']
initial_performance_df.set_index('ahead_days', inplace=True)
initial_performance_df


Out[44]:
GOOD_DATA_RATIO SAMPLES_GOOD_DATA_RATIO base_days model mre r2 step_days train_days train_val_time x_filename y_filename
ahead_days
1.0 0.99 0.9 112.0 random_forest 0.018002 0.984864 7.0 756.0 -1.0 x_base112_ahead1.pkl y_base112_ahead1.pkl
7.0 0.99 0.9 112.0 random_forest 0.044267 0.915048 7.0 756.0 -1.0 x_base112_ahead7.pkl y_base112_ahead7.pkl
14.0 0.99 0.9 112.0 random_forest 0.063327 0.829452 7.0 756.0 -1.0 x_base112_ahead14.pkl y_base112_ahead14.pkl
28.0 0.99 0.9 112.0 random_forest 0.096087 0.715802 7.0 756.0 -1.0 x_base112_ahead28.pkl y_base112_ahead28.pkl
56.0 0.99 0.9 112.0 random_forest 0.136095 0.512861 7.0 756.0 -1.0 x_base112_ahead56.pkl y_base112_ahead56.pkl

In [45]:
initial_performance_df.loc[14, 'base_days']


Out[45]:
112.0

Build the hyperparameters DataFrame


In [100]:
n_estimators = [10, 50, 100, 200]
max_depth = [None, 5, 10, 15]
hyper_df = pd.DataFrame([(x, y) for x in n_estimators for y in max_depth], columns=['n_estimators', 'max_depth'])
hyper_df['n_jobs'] = -1
hyper_df


Out[100]:
n_estimators max_depth n_jobs
0 10 NaN -1
1 10 5.0 -1
2 10 10.0 -1
3 10 15.0 -1
4 50 NaN -1
5 50 5.0 -1
6 50 10.0 -1
7 50 15.0 -1
8 100 NaN -1
9 100 5.0 -1
10 100 10.0 -1
11 100 15.0 -1
12 200 NaN -1
13 200 5.0 -1
14 200 10.0 -1
15 200 15.0 -1

In [101]:
params_df = initial_performance_df.loc[1]
params_df


Out[101]:
GOOD_DATA_RATIO                            0.99
SAMPLES_GOOD_DATA_RATIO                     0.9
base_days                                   112
model                             random_forest
mre                                   0.0180017
r2                                     0.984864
step_days                                     7
train_days                                  756
train_val_time                               -1
x_filename                 x_base112_ahead1.pkl
y_filename                 y_base112_ahead1.pkl
Name: 1.0, dtype: object

Ahead days = 1


In [96]:
AHEAD_DAYS = 1

# Get the normal parameters set
params_df = initial_performance_df.loc[AHEAD_DAYS].copy()
params_df['ahead_days'] = AHEAD_DAYS

tic = time()

from predictor.random_forest_predictor import RandomForestPredictor
PREDICTOR_NAME = 'random_forest'

# Global variables
eval_predictor_class = RandomForestPredictor
step_eval_days = 60  # The step to move between training/validation pairs

# Build the params list
params = {'params_df': params_df,
          'step_eval_days': step_eval_days,
          'eval_predictor_class': eval_predictor_class}

results_df = misc.parallelize_dataframe(hyper_df, misc.search_mean_score_eval, params)

# Some postprocessing... -----------------------------------------------------------
results_df['r2'] = results_df.apply(lambda x: x['scores'][0], axis=1)
results_df['mre'] = results_df.apply(lambda x: x['scores'][1], axis=1)
# Pickle that!
results_df.to_pickle('../../data/hyper_ahead{}_{}_df.pkl'.format(AHEAD_DAYS, PREDICTOR_NAME))
results_df['r2'].plot()

print('Minimum MRE param set: \n {}'.format(results_df.iloc[np.argmin(results_df['mre'])]))
print('Maximum R^2 param set: \n {}'.format(results_df.iloc[np.argmax(results_df['r2'])]))
# -----------------------------------------------------------------------------------

toc = time()
print('Elapsed time: {} seconds.'.format((toc-tic)))


Evaluating: {'n_estimators': 10, 'max_depth': 2, 'n_jobs': -1}
Generating: base112_ahead1_train756
Evaluating: {}
Evaluating: {}
Evaluating: {}
Evaluating approximately 77 training/evaluation pairs
Approximately 101.3 percent complete.    (0.73862156000194923, 0.083510910423665818)
Elapsed time: 259.04191064834595 seconds.

Ahead days = 7


In [102]:
AHEAD_DAYS = 7

# Get the normal parameters set
params_df = initial_performance_df.loc[AHEAD_DAYS].copy()
params_df['ahead_days'] = AHEAD_DAYS

tic = time()

from predictor.random_forest_predictor import RandomForestPredictor
PREDICTOR_NAME = 'random_forest'

# Global variables
eval_predictor_class = RandomForestPredictor
step_eval_days = 60  # The step to move between training/validation pairs

# Build the params list
params = {'params_df': params_df,
          'step_eval_days': step_eval_days,
          'eval_predictor_class': eval_predictor_class}

results_df = misc.parallelize_dataframe(hyper_df, misc.search_mean_score_eval, params)

# Some postprocessing... -----------------------------------------------------------
results_df['r2'] = results_df.apply(lambda x: x['scores'][0], axis=1)
results_df['mre'] = results_df.apply(lambda x: x['scores'][1], axis=1)
# Pickle that!
results_df.to_pickle('../../data/hyper_ahead{}_{}_df.pkl'.format(AHEAD_DAYS, PREDICTOR_NAME))
results_df['r2'].plot()

print('Minimum MRE param set: \n {}'.format(results_df.iloc[np.argmin(results_df['mre'])]))
print('Maximum R^2 param set: \n {}'.format(results_df.iloc[np.argmax(results_df['r2'])]))
# -----------------------------------------------------------------------------------

toc = time()
print('Elapsed time: {} seconds.'.format((toc-tic)))


Evaluating: {'n_estimators': 10.0, 'max_depth': None, 'n_jobs': -1.0}
Evaluating: {'n_estimators': 50.0, 'max_depth': None, 'n_jobs': -1.0}
Evaluating: {'n_estimators': 100.0, 'max_depth': None, 'n_jobs': -1.0}
Generating: base112_ahead7_train756
Evaluating: {'n_estimators': 200.0, 'max_depth': None, 'n_jobs': -1.0}
Generating: base112_ahead7_train756
Generating: base112_ahead7_train756
Generating: base112_ahead7_train756
Evaluating approximately 77 training/evaluation pairs
Evaluating approximately 77 training/evaluation pairs
Evaluating approximately 77 training/evaluation pairs
Evaluating approximately 77 training/evaluation pairs
Evaluating: {'n_estimators': 50, 'max_depth': None, 'n_jobs': -1}
Generating: base112_ahead7_train756
Evaluating: {'n_estimators': 100, 'max_depth': None, 'n_jobs': -1}
Generating: base112_ahead7_train756
Evaluating: {'n_estimators': 10, 'max_depth': None, 'n_jobs': -1}
Generating: base112_ahead7_train756
Evaluating: {'n_estimators': 200, 'max_depth': None, 'n_jobs': -1}
Generating: base112_ahead7_train756
Evaluating approximately 77 training/evaluation pairs
Evaluating approximately 77 training/evaluation pairs
Evaluating approximately 77 training/evaluation pairs
Evaluating approximately 77 training/evaluation pairs
Process ForkPoolWorker-110:
Process ForkPoolWorker-112:
Traceback (most recent call last):
Process ForkPoolWorker-113:
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 249, in _bootstrap
    self.run()
Traceback (most recent call last):
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 249, in _bootstrap
    self.run()
Traceback (most recent call last):
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 93, in run
    self._target(*self._args, **self._kwargs)
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 249, in _bootstrap
    self.run()
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 93, in run
    self._target(*self._args, **self._kwargs)
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py", line 119, in worker
    result = (True, func(*args, **kwds))
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 93, in run
    self._target(*self._args, **self._kwargs)
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py", line 44, in mapstar
    return list(map(*args))
Process ForkPoolWorker-111:
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-102-8e45f625e12d> in <module>()
     19           'eval_predictor_class': eval_predictor_class}
     20 
---> 21 results_df = misc.parallelize_dataframe(hyper_df, misc.search_mean_score_eval, params)
     22 
     23 # Some postprocessing... -----------------------------------------------------------

/home/miguel/udacity/Machine Learning Nanodegree/projects/capstone/capstone/utils/misc.py in parallelize_dataframe(params_df, func, params)
    122     df_split = np.array_split(params_df, NUM_PARTITIONS)
    123     pool = Pool(NUM_CORES)
--> 124     result_df = pd.concat(pool.map(partial(func, **params), df_split))
    125     pool.close()
    126     pool.join()

/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py in map(self, func, iterable, chunksize)
    258         in a list that is returned.
    259         '''
--> 260         return self._map_async(func, iterable, mapstar, chunksize).get()
    261 
    262     def starmap(self, func, iterable, chunksize=None):

/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py in get(self, timeout)
    600 
    601     def get(self, timeout=None):
--> 602         self.wait(timeout)
    603         if not self.ready():
    604             raise TimeoutError

/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py in wait(self, timeout)
    597 
    598     def wait(self, timeout=None):
--> 599         self._event.wait(timeout)
    600 
    601     def get(self, timeout=None):

/home/miguel/anaconda3/envs/cap_env/lib/python3.6/threading.py in wait(self, timeout)
    549             signaled = self._flag
    550             if not signaled:
--> 551                 signaled = self._cond.wait(timeout)
    552             return signaled
    553 

/home/miguel/anaconda3/envs/cap_env/lib/python3.6/threading.py in wait(self, timeout)
    293         try:    # restore state no matter what (e.g., KeyboardInterrupt)
    294             if timeout is None:
--> 295                 waiter.acquire()
    296                 gotit = True
    297             else:

KeyboardInterrupt: 
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py", line 119, in worker
    result = (True, func(*args, **kwds))
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py", line 119, in worker
    result = (True, func(*args, **kwds))
Traceback (most recent call last):
  File "../../utils/misc.py", line 108, in search_mean_score_eval
    axis=1)
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py", line 44, in mapstar
    return list(map(*args))
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/pool.py", line 44, in mapstar
    return list(map(*args))
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/multiprocessing/process.py", line 249, in _bootstrap
    self.run()
  File "/home/miguel/anaconda3/envs/cap_env/lib/python3.6/site-packages/pandas/core/frame.py", line 4360, in apply
    ignore_failures=ignore_failures)
  File "../../utils/misc.py", line 108, in search_mean_score_eval
    axis=1)
  File "../../utils/misc.py", line 108, in search_mean_score_eval
    axis=1)

Ahead days = 14


In [ ]:
AHEAD_DAYS = 14

# Get the normal parameters set
params_df = initial_performance_df.loc[AHEAD_DAYS].copy()
params_df['ahead_days'] = AHEAD_DAYS

tic = time()

from predictor.random_forest_predictor import RandomForestPredictor
PREDICTOR_NAME = 'random_forest'

# Global variables
eval_predictor_class = RandomForestPredictor
step_eval_days = 60  # The step to move between training/validation pairs

# Build the params list
params = {'params_df': params_df,
          'step_eval_days': step_eval_days,
          'eval_predictor_class': eval_predictor_class}

results_df = misc.parallelize_dataframe(hyper_df, misc.search_mean_score_eval, params)

# Some postprocessing... -----------------------------------------------------------
results_df['r2'] = results_df.apply(lambda x: x['scores'][0], axis=1)
results_df['mre'] = results_df.apply(lambda x: x['scores'][1], axis=1)
# Pickle that!
results_df.to_pickle('../../data/hyper_ahead{}_{}_df.pkl'.format(AHEAD_DAYS, PREDICTOR_NAME))
results_df['r2'].plot()

print('Minimum MRE param set: \n {}'.format(results_df.iloc[np.argmin(results_df['mre'])]))
print('Maximum R^2 param set: \n {}'.format(results_df.iloc[np.argmax(results_df['r2'])]))
# -----------------------------------------------------------------------------------

toc = time()
print('Elapsed time: {} seconds.'.format((toc-tic)))

Ahead days = 28


In [ ]:
AHEAD_DAYS = 28

# Get the normal parameters set
params_df = initial_performance_df.loc[AHEAD_DAYS].copy()
params_df['ahead_days'] = AHEAD_DAYS

tic = time()

from predictor.random_forest_predictor import RandomForestPredictor
PREDICTOR_NAME = 'random_forest'

# Global variables
eval_predictor_class = RandomForestPredictor
step_eval_days = 60  # The step to move between training/validation pairs

# Build the params list
params = {'params_df': params_df,
          'step_eval_days': step_eval_days,
          'eval_predictor_class': eval_predictor_class}

results_df = misc.parallelize_dataframe(hyper_df, misc.search_mean_score_eval, params)

# Some postprocessing... -----------------------------------------------------------
results_df['r2'] = results_df.apply(lambda x: x['scores'][0], axis=1)
results_df['mre'] = results_df.apply(lambda x: x['scores'][1], axis=1)
# Pickle that!
results_df.to_pickle('../../data/hyper_ahead{}_{}_df.pkl'.format(AHEAD_DAYS, PREDICTOR_NAME))
results_df['r2'].plot()

print('Minimum MRE param set: \n {}'.format(results_df.iloc[np.argmin(results_df['mre'])]))
print('Maximum R^2 param set: \n {}'.format(results_df.iloc[np.argmax(results_df['r2'])]))
# -----------------------------------------------------------------------------------

toc = time()
print('Elapsed time: {} seconds.'.format((toc-tic)))

Ahead days = 56


In [ ]:
AHEAD_DAYS = 56

# Get the normal parameters set
params_df = initial_performance_df.loc[AHEAD_DAYS].copy()
params_df['ahead_days'] = AHEAD_DAYS

tic = time()

from predictor.random_forest_predictor import RandomForestPredictor
PREDICTOR_NAME = 'random_forest'

# Global variables
eval_predictor_class = RandomForestPredictor
step_eval_days = 60  # The step to move between training/validation pairs

# Build the params list
params = {'params_df': params_df,
          'step_eval_days': step_eval_days,
          'eval_predictor_class': eval_predictor_class}

results_df = misc.parallelize_dataframe(hyper_df, misc.search_mean_score_eval, params)

# Some postprocessing... -----------------------------------------------------------
results_df['r2'] = results_df.apply(lambda x: x['scores'][0], axis=1)
results_df['mre'] = results_df.apply(lambda x: x['scores'][1], axis=1)
# Pickle that!
results_df.to_pickle('../../data/hyper_ahead{}_{}_df.pkl'.format(AHEAD_DAYS, PREDICTOR_NAME))
results_df['r2'].plot()

print('Minimum MRE param set: \n {}'.format(results_df.iloc[np.argmin(results_df['mre'])]))
print('Maximum R^2 param set: \n {}'.format(results_df.iloc[np.argmax(results_df['r2'])]))
# -----------------------------------------------------------------------------------

toc = time()
print('Elapsed time: {} seconds.'.format((toc-tic)))