<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
Step3:  Representation of events
just a list of start times + label, sentinel at the end
(+) easy to merge
(-) needs a sentinel
list of segments with start & end times and label
(+) provided eg. in the Isophonics dataset
(+) easy to reason about and visualize
(-) harder to merge
Step4:  [Weighted] Chord Symbol Recall
CSR = D_equals / D_annot
CSR - chord symbol recall (for a single song)
D_equals - total duration of segments where annotation equals estimation
D_annot - total duration of annotated segments
WSCR = sum(D_annot[i] * CSR[i])
WSCR - weighted CSR - average of CSR across songs weighted by song durations
<ASSISTANT_TASK:>
Python Code:
class Events():
    def __init__(self, start_times, labels):
        last item must be sentinel with no label
        assert(len(labels) >= len(start_times) - 1)
        if len(labels) < len(start_times):
            labels = labels.append(pd.Series([np.nan]))
        self._df = pd.DataFrame({'start': start_times, 'label': labels}, columns=['start', 'label'])
    def df(self):
        return self._df
    
class Segments():
    def __init__(self, start_times, labels):
        last item must be sentinel with NaN label
        self._df = segments_from_events(start_times, labels)
    def df(self):
        return self._df
    
    def join(self, other):
        sentinel_value = '_END_'
        def add_sentinel(df):
            last_event = df[-1:]
            return df.append(pd.DataFrame({
                'start': last_event['end'],
                'end': last_event['end'],
                'duration': 0.0,
                'label': sentinel_value
            }, columns=last_event.columns))
        def remove_sentinel(df, cols):
            for col in cols:
                df[col] = df[col].apply(lambda v: np.nan if v == sentinel_value else v)
        self_df = add_sentinel(self.df())[['start', 'label']].set_index('start')
        other_df = add_sentinel(other.df())[['start', 'label']].set_index('start')
        joined_df = self_df.join(other_df, lsuffix='_left', rsuffix='_right', how='outer')
        joined_df.fillna(method='ffill', inplace=True)
        remove_sentinel(joined_df, ['label_right', 'label_left'])
        joined_df['label_equals'] = joined_df['label_left'] == joined_df['label_right']
        joined_df.reset_index(inplace=True)
        joined_df['end'] = joined_df['start'].shift(-1)
        joined_df['duration'] = joined_df['end'] - joined_df['start']
        joined_df = joined_df[:-1]
        return joined_df #Segments(joined_df['start'], joined_df['label'])
annotations = Segments(np.array([0, 1, 2, 3, 3.5, 4]), ['A','B','A','C','A'])
annotations.df()
plot_segments(annotations.df())
estimations = Segments(np.array([0, 0.9, 1.8, 2.5, 3.1, 3.4, 4.5]), ['A','B','A','B','C','A'])
estimations.df()
plot_segments(estimations.df())
def join_segments(df1, df2):
    Joins two dataframes with segments into a single one (ignoring labels)
    np.hstack(events_from_segments(df1), events_from_segments(df1))
events = np.hstack([events_from_segments(annotations), events_from_segments(estimations)])
events.sort()
events = np.unique(events)
events
merged = segments_from_events(events)
merged
plot_segments(merged)
merged_df = annotations.join(estimations)
merged_df
Explanation: Representation of events
just a list of start times + label, sentinel at the end
(+) easy to merge
(-) needs a sentinel
list of segments with start & end times and label
(+) provided eg. in the Isophonics dataset
(+) easy to reason about and visualize
(-) harder to merge
End of explanation
def chord_symbol_recall(pred_segments, true_segments):
    merged_df = pred_segments.join(true_segments)
    return merged_df[merged_df['label_equals']]['duration'].sum() / merged_df['duration'].sum()
chord_symbol_recall(estimations, annotations)
Explanation: [Weighted] Chord Symbol Recall
CSR = D_equals / D_annot
CSR - chord symbol recall (for a single song)
D_equals - total duration of segments where annotation equals estimation
D_annot - total duration of annotated segments
WSCR = sum(D_annot[i] * CSR[i])
WSCR - weighted CSR - average of CSR across songs weighted by song durations
End of explanation
<END_TASK>