<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Extractors Tutorial
Introduction
This tutorial is a guide of how you can create your custom feature extraction routine
and add this extractor to feets.
Fundamentals
The feature extraction as modeled as a class.
The class must inerith from feets.Extractor
The extractor class must has at least three elements
data
Step1:  Finally to make the extractor available for the FeaturSpace class, you need to register it with the command
Step2:  Now the extractor are available as any other provided in feets
Step3:  Example 2
Step4:  The interesting thing about this extractor is that it is selected by the FeatureSpace regardless of whether we request for the error or not (In both cases the new extractor will be at the end of the set)
Step5:  Now let's try to use this extractor to calculate the average value of 100 random quantities between 12 and 14
and a normal distribution of errors
Step6:  We can see that the average magnitude is
Step7:  And using the error as weight is
Step8:  Now we create the FeatureSpace only with the extractor of interest
Step9:  And we can verify the difference of providing the error
Step10:  Or use only the magnitude
Step11:  Example 3
Step12:  now we can create the FeatureSpace instance
Step13:  And finally, we can extract robust_mean from a random uniform magnitudes.
Step14:  Now let's assume that we want a configurable extractor that allows the user to determine which percentiles to remove before calculating the average. This is possible thanks to the params attribute.
params must be a dict that has the name of the parameter as a key, and its default value. All the keys of the
dict params are sent as parameters to thefit ()method in conjunction with the required time-series data.
Now if we want to write the same RobustMean but with a configurable parameter instead of leaving the $ 5 \% $ fixed, we can do it as follows.
Step15:  The parameter percentile of our extractor, is configurable, at the time of
create our FeatureSpace.
The way to change the value is to provide the class name as a keyword argument, and all the params that you want to change
Like a dictionary
Step16:  Example 4
Step17:  The interesting thing is that if we call the method as_arrays() or as_dataframe() each one of the values the matrix is split into independent elements.
Step18:  The conversion of a 2D matrix to scalars is executed in the flatten_feature () method of the extractor; whose default behavior works for arrays of an arbitrary dimension by adding suffixes for each dimension.
Thus the value of the matrix Mag_Err_Histogram[i][j] becomes a feature Mag_Err_Histogram_i_j (if there were more dimensions only more sub-indices are added). If it's needed a custom behavior can be provided.
For example, if we wanted to return only an average value of the histogram; We could redefine this method as follows
Step19:  The flatten_feature method receives several parameters
Step20:  We can plot the SignaturePhMag with the next command.
Step21:  But not only that, but we also provide full integration with Matplotlib
Step22:  The catch is, by default all the extractors can't plot their features.
Let's try to plot the feature from the first example magmax or mintime
<ASSISTANT_TASK:>
Python Code:
import feets
class MaxMagMinTime(feets.Extractor):  # must inherit from Extractor
    data = ['magnitude', 'time']  # Which data is needed 
                                  # to calculate this feature
    
    features = ["magmax", "mintime"] # The names of the expected 
                                     # feature
    
    # The values of data are the params
    def fit(self, magnitude, time):        
        # The return value must be a dict with the same values 
        # defined in  features
        return {"magmax": magnitude.max(), "mintime": time.min()}  
Explanation: Extractors Tutorial
Introduction
This tutorial is a guide of how you can create your custom feature extraction routine
and add this extractor to feets.
Fundamentals
The feature extraction as modeled as a class.
The class must inerith from feets.Extractor
The extractor class must has at least three elements
data: list with at least one valid feets data 
   (time, magnitude, error, magnitude2, aligned_time, aligned_magnitude, aligned_magnitude2, 
   aligned_error or aligned_error2)
features: list with the name of the features that this extractor generates.
fit: a method with the same parameters defined in the data list. fit() must return
   a dictionary with keys equals to the features list.
Example 1: MaxMagMinTime extractor
Let's say we need to create a feature extractor called MaxMagMinTime that must return 2 features:
magmax: The maximun magnitude 
mintime: the minimun time
End of explanation
feets.register_extractor(MaxMagMinTime)
Explanation: Finally to make the extractor available for the FeaturSpace class, you need to register it with the command:
End of explanation
# let's create the feature-space
fs = feets.FeatureSpace(only=["magmax", "mintime"])
fs
# extract the features
fs.extract(time=[1,2,3], magnitude=[100, 200, 300])
Explanation: Now the extractor are available as any other provided in feets:
End of explanation
class WeightedMean(feets.Extractor):
    data = ['magnitude', 'error']
    optional = ['error']
    
    features = ["weighted_mean"] 
    
    # if error is not provided to the FeatureSpace,
    # the value of erro will be None.
    def fit(self, magnitude, error):
        if error is None:
            weighted_mean = np.average(magnitude) 
        else:
            weighted_mean = np.average(magnitude, weights=error)
        return {"weighted_mean": weighted_mean}  
feets.register_extractor(WeightedMean)
Explanation: Example 2: WeightedMean extractor - Optional data.
Any extractor can set some parameters of the light curves as optional. For this, the Extractors provide the optional attribute which is a list that any component defined in data can have.
For example, if we wanted to make the WeightedMean extractor which calculates the average of the magnitudes optionally weighted by the error:
End of explanation
fs = feets.FeatureSpace(data=["magnitude", "error"])
fs.features_extractors_
fs = feets.FeatureSpace(data=["magnitude"])
fs.features_extractors_
Explanation: The interesting thing about this extractor is that it is selected by the FeatureSpace regardless of whether we request for the error or not (In both cases the new extractor will be at the end of the set):
End of explanation
import numpy as np
magnitude = np.random.uniform(12, 14, size=100)
error = np.random.normal(size=100)
Explanation: Now let's try to use this extractor to calculate the average value of 100 random quantities between 12 and 14
and a normal distribution of errors
End of explanation
np.average(magnitude)
Explanation: We can see that the average magnitude is
End of explanation
np.average(magnitude, weights=error)
Explanation: And using the error as weight is
End of explanation
fs = feets.FeatureSpace(only=["weighted_mean"])
Explanation: Now we create the FeatureSpace only with the extractor of interest
End of explanation
fs.extract(magnitude=magnitude, error=error).as_dict()
Explanation: And we can verify the difference of providing the error
End of explanation
fs.extract(magnitude=magnitude).as_dict()
Explanation: Or use only the magnitude
End of explanation
class RobustMean(feets.Extractor):
    data = ["magnitude"]
    features = ["robust_mean"]
    
    def fit(self, magnitude):
        # extract the percentiles
        llimit, ulimit = np.percentile(magnitude, (5, 95))
        
        # remove the two limits
        crop = magnitude[
            (magnitude > llimit) & (magnitude < ulimit)]
        
        # calculate the mean
        robust_mean = np.mean(crop)
        
        return{"robust_mean": robust_mean}
    
feets.register_extractor(RobustMean)
Explanation: Example 3: RobustMean extractor - Configuration parameters
Let's assume that we want to make an extractor that calculates an average over the magnitudes but in this case, omit the extreme values. For example, remove all values of the upper and lower $ 5 \% $ from the quantities. For that, we can use the numpy percentile function
End of explanation
fs = feets.FeatureSpace(only=["robust_mean"])
fs
Explanation: now we can create the FeatureSpace instance
End of explanation
magnitude = np.random.uniform(12, 14, size=100)
fs.extract(magnitude=magnitude)["robust_mean"]
Explanation: And finally, we can extract robust_mean from a random uniform magnitudes.
End of explanation
class RobustMean(feets.Extractor):
    data = ["magnitude"]
    features = ["robust_mean"]
        
    # by default the percentile to crop is still 5.
    params = {"percentile": 5}
    
    # now magnitude (from data), and percentile (from params)
    # are are given as keyword argument.
    def fit(self, magnitude, percentile):
        # first calculate the lower and upper percentile
        lp, up = percentile, 100 - percentile
        
        # extract the percentiles
        llimit, ulimit = np.percentile(magnitude, (lp, up))
        
        # remove the two limits
        crop = magnitude[
            (magnitude > llimit) & (magnitude < ulimit)]
        
        # calculate the mean
        robust_mean = np.mean(crop)
        
        return{"robust_mean": robust_mean}
    
feets.register_extractor(RobustMean)
Explanation: Now let's assume that we want a configurable extractor that allows the user to determine which percentiles to remove before calculating the average. This is possible thanks to the params attribute.
params must be a dict that has the name of the parameter as a key, and its default value. All the keys of the
dict params are sent as parameters to thefit ()method in conjunction with the required time-series data.
Now if we want to write the same RobustMean but with a configurable parameter instead of leaving the $ 5 \% $ fixed, we can do it as follows.
End of explanation
fs = feets.FeatureSpace(only=["robust_mean"], RobustMean={"percentile": 6})
fs
fs.extract(magnitude=magnitude).as_dict()
Explanation: The parameter percentile of our extractor, is configurable, at the time of
create our FeatureSpace.
The way to change the value is to provide the class name as a keyword argument, and all the params that you want to change
Like a dictionary
End of explanation
@feets.register_extractor
class MagErrHistogram(feets.Extractor):
    data = ["magnitude", "error"]
    features = ["Mag_Err_Histogram"]
    
    def fit(self, magnitude, error):
        histogram2d = np.histogram2d(magnitude, error)[0]
        return {"Mag_Err_Histogram": histogram2d}
fs = feets.FeatureSpace(only=["Mag_Err_Histogram"])
magnitude = np.random.uniform(12, 14, size=100)
error = np.random.normal(size=100)
rs = fs.extract(magnitude=magnitude, error=error)
rs.as_dict()
Explanation: Example 4: MagErrHistogram extractor - Flatten API
The features generated by the extractors can be any arbitrary Python object. 
For example the following extractor binarizes the values of magnitude anderror in a 2D histogram, and returns them in the form of a 2-dimensional array:
End of explanation
rs.as_dataframe()
Explanation: The interesting thing is that if we call the method as_arrays() or as_dataframe() each one of the values the matrix is split into independent elements.
End of explanation
@feets.register_extractor
class MagErrHistogram(feets.Extractor):
    data = ["magnitude", "error"]
    features = ["Mag_Err_Histogram"]
    
    def flatten_feature(self, feature, value, extractor_features, **kwargs):
        return {"Mag_Err_Histogram": np.mean(value)}
    
    def fit(self, magnitude, error):
        histogram2d = np.histogram2d(magnitude, error)[0]
        return {"Mag_Err_Histogram": histogram2d}
fs = feets.FeatureSpace(only=["Mag_Err_Histogram"])
rs = fs.extract(magnitude=magnitude, error=error)
rs.as_dataframe()
Explanation: The conversion of a 2D matrix to scalars is executed in the flatten_feature () method of the extractor; whose default behavior works for arrays of an arbitrary dimension by adding suffixes for each dimension.
Thus the value of the matrix Mag_Err_Histogram[i][j] becomes a feature Mag_Err_Histogram_i_j (if there were more dimensions only more sub-indices are added). If it's needed a custom behavior can be provided.
For example, if we wanted to return only an average value of the histogram; We could redefine this method as follows:
End of explanation
time = np.arange(100) + np.random.normal(size=100, loc=10)
magnitude = np.random.uniform(12, 14, size=100)
error = np.random.normal(size=100)
fs = feets.FeatureSpace(data=["magnitude", "time", "error"])
rs = fs.extract(time=time, magnitude=magnitude, error=error)
Explanation: The flatten_feature method receives several parameters:
    
- feature: the name of the feature you are trying to flatten. It is useful if the extractor generates several features and we want to provide a different logic for each one.
- value: the value of the feature to flatten as it was generated.
- extractor_features: All the features generated by this extractor. It is useful if the flattening logic depends on the values of other features.
- **kwargs: All parameters received by the fit() method. This is all time-serie plus all configurable params. It is useful if the flattening logic depends on the values of the time series or the configuration of the extractor.
Example 5: Plots
One of the feets strengths is the ability to explore the features. 
In addition to converting FeatureSet to something like the Pandas DataFrames; feets offers the possibility of generating plots for each feature.
For example, if we extract the features of a random light curve
End of explanation
rs.plot("SignaturePhMag");
Explanation: We can plot the SignaturePhMag with the next command.
End of explanation
fig, ax = plt.subplots(figsize=(5, 5))
rs.plot("SignaturePhMag", ax=ax, cmap="viridis_r");
Explanation: But not only that, but we also provide full integration with Matplotlib:
We can, for example, provide the axis to plot (ax).
Or change the color palette (cmap).
In this case, any extra parameters that we give to the plot method for the SignaturePhMag feature will be sent to the function.
seaborn.heatmap.
End of explanation
rs.plot("magmax")
Explanation: The catch is, by default all the extractors can't plot their features.
Let's try to plot the feature from the first example magmax or mintime
End of explanation
<END_TASK>