Repository: unit8co/darts

Issue:
[BUG] Unhelpful error message on TimeSeries.with_hierarchy
**Describe the bug**
When encoding the hierarchy the time series hierarchy we need to construct a hierarchy dict of the format `{child_node: [parent_node1, parent_node2, ...]}`. 
However, if there is only one parent node in the whole TimeSeries it is easy to make the mistake of constructing a dict `{child_node: parent_node}` instead of `{child_node: [parent_node}`. 
Doing so leads to the following rather unhelpful error message:
`ValueError: The values of the hierarchy must only contain component names matching those of the series.`

**To Reproduce**
```python
dates = pd.date_range("2020-01-01", "2020-12-31", freq="D")
nr_dates = len(dates)
t1 = TimeSeries.from_times_and_values(dates, 3 * np.ones(nr_dates), columns=["T1"])
t2 = TimeSeries.from_times_and_values(dates, 5 * np.ones(nr_dates), columns=["T2"])
t3 = TimeSeries.from_times_and_values(dates, np.ones(nr_dates), columns=["T3"])
tsum = TimeSeries.from_times_and_values(dates, 9 * np.ones(nr_dates), columns=["T_sum"])

ts = concatenate([t1, t2, t3, tsum], axis="component")
hierarchy = {"T1": "T_sum", "T2": "T_sum", "T3": "T_sum"}
ts = ts.with_hierarchy(hierarchy)
```

**Expected behavior**
Either it should simply detect that one component was specified rather than a list and internally convert it to a list, or a helpful error message should be issued

**System (please complete the following information):**
 - Python version: 3.10.6
 - darts version: master

**Additional context**
I think there's two possibilities to solve this
- Detect strings and convert them to a list of one string
- Raise an error message that instructs the user to use `["string"]` instead of `"string"` when constructing the hierarchy dict

I'm happy to submit a PR if you let me know which of these options is preferred


Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co) Report
Base: **94.06**% // Head: **94.02**% // Decreases project coverage by **`-0.05%`** :warning:
> Coverage data is based on head [(`21fe009`)](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co) compared to base [(`690b6f4`)](https://codecov.io/gh/unit8co/darts/commit/690b6f409de32fa728e4fa419a63436604ac4ccc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co).
> Patch coverage: 100.00% of modified lines in pull request are covered.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1584      +/-   ##
==========================================
- Coverage   94.06%   94.02%   -0.05%     
==========================================
  Files         125      125              
  Lines       11095    11083      -12     
==========================================
- Hits        10437    10421      -16     
- Misses        658      662       +4     
```


| [Impacted Files](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co) | Coverage Δ | |
|---|---|---|
| [darts/timeseries.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvdGltZXNlcmllcy5weQ==) | `92.16% <100.00%> (-0.22%)` | :arrow_down: |
| [darts/utils/data/tabularization.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvdXRpbHMvZGF0YS90YWJ1bGFyaXphdGlvbi5weQ==) | `99.27% <0.00%> (-0.73%)` | :arrow_down: |
| [darts/ad/anomaly\_model/filtering\_am.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvYWQvYW5vbWFseV9tb2RlbC9maWx0ZXJpbmdfYW0ucHk=) | `91.93% <0.00%> (-0.13%)` | :arrow_down: |
| [...arts/models/forecasting/torch\_forecasting\_model.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvbW9kZWxzL2ZvcmVjYXN0aW5nL3RvcmNoX2ZvcmVjYXN0aW5nX21vZGVsLnB5) | `89.52% <0.00%> (-0.05%)` | :arrow_down: |
| [darts/models/forecasting/block\_rnn\_model.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvbW9kZWxzL2ZvcmVjYXN0aW5nL2Jsb2NrX3Jubl9tb2RlbC5weQ==) | `98.24% <0.00%> (-0.04%)` | :arrow_down: |
| [darts/models/forecasting/nhits.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvbW9kZWxzL2ZvcmVjYXN0aW5nL25oaXRzLnB5) | `99.27% <0.00%> (-0.01%)` | :arrow_down: |
| [darts/datasets/\_\_init\_\_.py](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co#diff-ZGFydHMvZGF0YXNldHMvX19pbml0X18ucHk=) | `100.00% <0.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/unit8co/darts/pull/1584?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=unit8co).

DavidKleindienst: Thanks for reviewing @dennisbader 
I've added the changes you suggested
dennisbader: Thanks! Ready to merge 🚀 

Patch:
diff --git a/darts/timeseries.py b/darts/timeseries.py
index 6d02bbdb..4fb2f9c0 100644
--- a/darts/timeseries.py
+++ b/darts/timeseries.py
@@ -268,6 +268,12 @@ class TimeSeries:
             # pre-compute grouping informations
             components_set = set(self.components)
             children = set(hierarchy.keys())
+
+            # convert string ancestors to list of strings
+            hierarchy = {
+                k: ([v] if isinstance(v, str) else v) for k, v in hierarchy.items()
+            }
+
             raise_if_not(
                 all(c in components_set for c in children),
                 "The keys of the hierarchy must be time series components",
@@ -2851,7 +2857,7 @@ class TimeSeries:
             )
         )
 
-    def with_hierarchy(self, hierarchy: Dict):
+    def with_hierarchy(self, hierarchy: Dict[str, Union[str, list[str]]]):
         """
         Adds a hierarchy to the TimeSeries.
 
@@ -2859,6 +2865,7 @@ class TimeSeries:
         ----------
         hierarchy
             A dictionary mapping components to a list of their parent(s) in the hierarchy.
+            Single parents may be specified as string or list containing one string.
             For example, assume the series contains the components
             ``["total", "a", "b", "x", "y", "ax", "ay", "bx", "by"]``,
             the following dictionary would encode the groupings shown on
@@ -2873,10 +2880,11 @@ class TimeSeries:
                              'by': ['b', 'y'],
                              'a': ['total'],
                              'b': ['total'],
-                             'x': ['total'],
-                             'y': ['total']}
+                             'x': 'total',  # or use a single string
+                             'y': 'total'}
             ..
 
+
         """
 
         return self.__class__(


Test patch:
diff --git a/darts/tests/test_timeseries.py b/darts/tests/test_timeseries.py
index 975e4a23..55319b26 100644
--- a/darts/tests/test_timeseries.py
+++ b/darts/tests/test_timeseries.py
@@ -1845,6 +1845,30 @@ class TimeSeriesHierarchyTestCase(DartsBaseTestClass):
             (series1.slice_intersect(series2[10:20])).hierarchy, self.hierarchy
         )
 
+    def test_with_string_items(self):
+        # Single parents may be specified as string rather than [string]
+        dates = pd.date_range("2020-01-01", "2020-12-31", freq="D")
+        nr_dates = len(dates)
+        t1 = TimeSeries.from_times_and_values(
+            dates, 3 * np.ones(nr_dates), columns=["T1"]
+        )
+        t2 = TimeSeries.from_times_and_values(
+            dates, 5 * np.ones(nr_dates), columns=["T2"]
+        )
+        t3 = TimeSeries.from_times_and_values(dates, np.ones(nr_dates), columns=["T3"])
+        tsum = TimeSeries.from_times_and_values(
+            dates, 9 * np.ones(nr_dates), columns=["T_sum"]
+        )
+
+        ts = concatenate([t1, t2, t3, tsum], axis="component")
+        string_hierarchy = {"T1": "T_sum", "T2": "T_sum", "T3": "T_sum"}
+        ts_with_string_hierarchy = ts.with_hierarchy(string_hierarchy)
+        hierarchy_as_list = {k: [v] for k, v in string_hierarchy.items()}
+        assert ts_with_string_hierarchy.hierarchy == hierarchy_as_list
+        list_hierarchy = {"T1": ["T_sum"], "T2": ["T_sum"], "T3": ["T_sum"]}
+        ts_with_list_hierarchy = ts.with_hierarchy(list_hierarchy)
+        assert ts_with_string_hierarchy.hierarchy == ts_with_list_hierarchy.hierarchy
+
 
 class TimeSeriesHeadTailTestCase(DartsBaseTestClass):