Repository: ludwig-ai/ludwig

Issue:
ZeroDivisionError in audio feature prediction with only  a single record
**Describe the bug**
When generating a model prediction  involving an audio feature and the data set contains only one record, a `ZeroDvisionError` exception is raised in `ludwig.utils.audio_utils.calcuate_var()` function.  The error occurs because the variable `count = 1` which results in a zero in the denominator of this expression:
```
return (sum2 - ((sum1 * sum1) / float(count))) / float(count - 1)
```

**To Reproduce**
Steps to reproduce the behavior:
Run unit test `test_server.py`


**Expected behavior**
Successful prediction with only a single record.

**Log file**
Here is log and error messages:
```
PASSED [ 66%]FAILED [100%]Failed to run predict: float division by zero
Traceback (most recent call last):
  File "/opt/project/ludwig/serve.py", line 92, in predict
    dataset=[entry], data_format=dict
  File "/opt/project/ludwig/api.py", line 683, in predict
    backend=self.backend,
  File "/opt/project/ludwig/data/preprocessing.py", line 1728, in preprocess_for_prediction
    backend
  File "/opt/project/ludwig/data/preprocessing.py", line 162, in preprocess_for_prediction
    backend=backend
  File "/opt/project/ludwig/data/preprocessing.py", line 1080, in build_dataset
    skip_save_processed_input
  File "/opt/project/ludwig/data/preprocessing.py", line 1225, in build_data
    skip_save_processed_input
  File "/opt/project/ludwig/features/audio_feature.py", line 343, in add_feature_data
    backend
  File "/opt/project/ludwig/features/audio_feature.py", line 174, in _process_in_memory
    merged_stats['var'] = calculate_var(merged_stats['sum'], merged_stats['sum2'], merged_stats['count'])
  File "/opt/project/ludwig/utils/audio_utils.py", line 240, in calculate_var
    return (sum2 - ((sum1 * sum1) / float(count))) / float(count - 1)
ZeroDivisionError: float division by zero

tests/integration_tests/test_server.py:193 (test_server_integration_with_audio[True])
500 != 200

Expected :200
Actual   :500
<Click to see difference>

single_record = True, csv_filename = 'EA8A7B56DE.csv'

    @pytest.mark.parametrize('single_record', [False, True])
    def test_server_integration_with_audio(single_record, csv_filename):
        # Audio Inputs
        audio_dest_folder = os.path.join(os.getcwd(), 'generated_audio')
    
        # Resnet encoder
        input_features = [
            audio_feature(
                folder=audio_dest_folder,
            ),
            text_feature(encoder='embed', min_len=1),
            numerical_feature(normalization='zscore')
        ]
        output_features = [
            category_feature(vocab_size=2),
            numerical_feature()
        ]
    
        rel_path = generate_data(input_features, output_features, csv_filename)
        model, output_dir = train_model(input_features, output_features,
                                        data_csv=rel_path)
    
        app = server(model)
        client = TestClient(app)
        response = client.get('/')
        assert response.status_code == 200
    
        response = client.post('/predict')
        # expect the HTTP 400 error code for this situation
        assert response.status_code == 400
        assert response.json() == ALL_FEATURES_PRESENT_ERROR
    
        data_df = read_csv(rel_path)
    
        if single_record:
            # Single record prediction
            first_entry = data_df.T.to_dict()[0]
            data, files = convert_to_form(first_entry)
            server_response = client.post('/predict', data=data, files=files)
>           assert server_response.status_code == 200
E           assert 500 == 200

../../tests/integration_tests/test_server.py:233: AssertionError

```

**Environment (please complete the following information):**
 - OS: [e.g. iOS]  Ludwig Docker container
 - Version [e.g. 22]
- Python version: 3.6.9
- Ludwig version: 0.4-dev0

**Additional context**
As short-term work-around modified denominator to be `max(1, count-1)`.  



Discussion:

Resolving patch:
diff --git a/ludwig/features/audio_feature.py b/ludwig/features/audio_feature.py
--- a/ludwig/features/audio_feature.py
+++ b/ludwig/features/audio_feature.py
@@ -188,9 +188,29 @@ def reduce(series):
             return merged_stats
 
         merged_stats = df_engine.reduce_objects(audio_stats, reduce)
-        merged_stats['mean'] = calculate_mean(merged_stats['sum'], merged_stats['count'])
-        merged_stats['var'] = calculate_var(merged_stats['sum'], merged_stats['sum2'], merged_stats['count'])
-        return processed_audio, merged_stats
+        merged_stats['mean'] = calculate_mean(merged_stats['sum'],
+                                              merged_stats['count'])
+        merged_stats['var'] = calculate_var(merged_stats['sum'],
+                                            merged_stats['sum2'],
+                                            merged_stats['count'])
+        merged_stats['std'] = np.sqrt(
+            merged_stats['var'] / float(merged_stats['count']))
+        print_statistics = (
+            "{} audio files loaded.\n"
+            "Statistics of audio file lengths:\n"
+            "- mean: {:.4f}\n"
+            "- std: {:.4f}\n"
+            "- max: {:.4f}\n"
+            "- min: {:.4f}\n"
+            "- cropped audio_files: {}\n"
+            "Max length was given as {}s"
+        ).format(
+            merged_stats['count'], merged_stats['mean'],
+            merged_stats['std'], merged_stats['max'],
+            merged_stats['min'], merged_stats['cropped'],
+            audio_file_length_limit_in_s)
+        logger.debug(print_statistics)
+        return processed_audio
 
     @staticmethod
     def _transform_to_feature(
@@ -349,7 +369,7 @@ def add_feature_data(
                 'There are no audio files in the dataset provided.')
 
         if feature[PREPROCESSING]['in_memory']:
-            audio_features, audio_stats = AudioFeatureMixin._process_in_memory(
+            audio_features = AudioFeatureMixin._process_in_memory(
                 input_df[feature[NAME]],
                 src_path,
                 audio_feature_dict,
@@ -361,24 +381,6 @@ def add_feature_data(
                 backend
             )
             proc_df[proc_column] = audio_features
-
-            audio_stats['std'] = np.sqrt(
-                audio_stats['var'] / float(audio_stats['count']))
-            print_statistics = (
-                "{} audio files loaded.\n"
-                "Statistics of audio file lengths:\n"
-                "- mean: {:.4f}\n"
-                "- std: {:.4f}\n"
-                "- max: {:.4f}\n"
-                "- min: {:.4f}\n"
-                "- cropped audio_files: {}\n"
-                "Max length was given as {}s"
-            ).format(
-                audio_stats['count'], audio_stats['mean'],
-                audio_stats['std'], audio_stats['max'],
-                audio_stats['min'], audio_stats['cropped'],
-                audio_file_length_limit_in_s)
-            logger.debug(print_statistics)
         else:
             backend.check_lazy_load_supported(feature)
 
diff --git a/ludwig/utils/audio_utils.py b/ludwig/utils/audio_utils.py
--- a/ludwig/utils/audio_utils.py
+++ b/ludwig/utils/audio_utils.py
@@ -237,9 +237,8 @@ def calculate_incr_mean(count, mean, length):
 
 
 def calculate_var(sum1, sum2, count):
-    # todo: revert from 'max(1, count -1)' back to 'count - 1' in denominator
-    #    when GH Issue #1181 is addressed
-    return (sum2 - ((sum1 * sum1) / float(count))) / float(max(1, count - 1))
+    return (sum2 - ((sum1 * sum1) / float(count))) / float(count - 1) \
+        if count > 1 else 0.0
 
 
 def calculate_mean(sum1, count):


Test patch: