Generate classname from variable in template
Valid CSS identifiers (for example, element names, classes, and IDs in selectors) cannot start with a digit, or a hyphen followed by a digit. Html::cleanCssIdentifier(), which is called when a CSS identifier is created from a string, contains the following code.
  // Valid characters in a CSS identifier are:
  // - the hyphen (U+002D)
  // - a-z (U+0030 - U+0039)
  // - A-Z (U+0041 - U+005A)
  // - the underscore (U+005F)
  // - 0-9 (U+0061 - U+007A)
  // - ISO 10646 characters U+00A1 and higher
  // We strip out any character not in the above list.
  $identifier = preg_replace('/[^\\x{002D}\\x{0030}-\\x{0039}\\x{0041}-\\x{005A}\\x{005F}\\x{0061}-\\x{007A}\\x{00A1}-\\x{FFFF}]/u', '', $identifier);

  // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
  $identifier = preg_replace([
    '/^[0-9]/',
    '/^(-[0-9])|^(--)/',
  ], [
    '_',
    '__',
  ], $identifier);

As a digit is only replaced when it's the first character, or the second character when the first character is a hyphen, it's enough to  first concatenate a string that is an acceptable CSS identifier, as in the following code.
{% if title %}
  {%
    set classes = [
      'views-group',
      ('group-' ~ title)|clean_class,
    ]
  %}
  &lt;h3{{ content_attributes.addClass(classes) }}&gt;{{ title }}&lt;/h3&gt;
{% endif %}
{% for row in rows %}
  {%
    set row_classes = [
      default_row_class ? 'views-row',
    ]
  %}
  &lt;div{{ row.attributes.addClass(row_classes) }}&gt;
    {{- row.content -}}
  &lt;/div&gt;
{% endfor %}

The only difference with the code shown in the question is using ('group-' ~ title)|clean_class, instead of 'group-' ~ title|clean_class. The parentheses are necessary because | takes precedence over ~.
That is the only change needed to avoid that the first digit in the node title is replaced with an underscore. There is no need to change the code to add a single CSS class instead of two CSS classes.