Repository: tskit-dev/msprime (language: python)

Issue:
Change missing parent value to "." in simple Pedigree text format
For compatibility with incoming sgkit pedigree format https://github.com/pystatgen/sgkit/pull/839


Discussion / hints:

Fix patch:
diff --git a/docs/ancestry.md b/docs/ancestry.md
index 2777537c2..5b0b5a312 100644
--- a/docs/ancestry.md
+++ b/docs/ancestry.md
@@ -2381,8 +2381,8 @@ ped_txt = """\
 8   9   9   2.0 0
 9   11  12  3.0 0
 10  11  12  3.0 0
-11  NA  NA  4.0 0
-12  NA  NA  4.0 0
+11  .   .   4.0 0
+12  .   .   4.0 0
 """
 pedigree = msprime.parse_pedigree(io.StringIO(ped_txt), sequence_length=100)
 
@@ -2485,12 +2485,12 @@ ped_txt = """\
 4   8   8   1.0 0
 5   8   6   1.0 0
 6   9   10  2.0 0
-7   NA  NA  2.0 0
+7   .   .   2.0 0
 8   9   9   2.0 0
 9   11  12  3.0 0
 10  11  12  3.0 0
-11  NA  NA  4.0 0
-12  NA  NA  4.0 0
+11  .   .   4.0 0
+12  .   .   4.0 0
 """
 pedigree = msprime.parse_pedigree(io.StringIO(ped_txt), sequence_length=100)
 
@@ -2563,12 +2563,12 @@ ped_txt = """\
 9   12  13  1.0 0   A
 10  14  12  1.0 0   A
 11  15  16  1.0 0   A
-12  NA  NA  2.0 0   A
-13  NA  NA  2.0 0   A
-14  NA  NA  2.0 0   A
-15  NA  NA  2.0 0   A
-16  NA  NA  2.0 0   A
-17  NA  NA  2.0 0   A
+12  .   .   2.0 0   A
+13  .   .   2.0 0   A
+14  .   .   2.0 0   A
+15  .   .   2.0 0   A
+16  .   .   2.0 0   A
+17  .   .   2.0 0   A
 18  23  22  0.0 1   B
 19  22  22  0.0 1   B
 20  21  22  0.0 1   B
@@ -2582,8 +2582,8 @@ ped_txt = """\
 28  31  32  4.0 0   B
 29  31  32  4.0 0   B
 30  32  31  4.0 0   B
-31  NA  NA  5.0 0   B
-32  NA  NA  5.0 0   B
+31  .   .   5.0 0   B
+32  .   .   5.0 0   B
 """
 pedigree = msprime.parse_pedigree(
     io.StringIO(ped_txt), demography=demography, sequence_length=100)
diff --git a/docs/pedigrees.md b/docs/pedigrees.md
index 0e388ab49..a71537322 100644
--- a/docs/pedigrees.md
+++ b/docs/pedigrees.md
@@ -96,7 +96,7 @@ returns the integer ID of newly added individual.
 The methods described in {ref}`sec_pedigrees_encoding` are general and
 allow for arbitrary metadata to be associated with individuals. It
 is often convenient to work with a text based representation of
-the pedigree, which is supported by the {func}`.parse_pedigree`.
+the pedigree, which is supported by the {func}`.parse_pedigree` function.
 
 ### Basic structure
 
@@ -104,8 +104,8 @@ the pedigree, which is supported by the {func}`.parse_pedigree`.
 txt = """\
 # id parent0 parent1 time
 child mom dad 0
-mom NA NA 1
-dad NA NA 1
+mom    .   .  1
+dad    .   .  1
 """
 pedigree = msprime.parse_pedigree(io.StringIO(txt))
 display(pedigree.individuals)
@@ -122,8 +122,8 @@ import io
 txt = """\
 # id parent0 parent1 time
 child mom dad 0
-mom NA NA 1
-dad NA NA 1
+mom    .   .  1
+dad    .   .  1
 """
 pedigree = msprime.parse_pedigree(io.StringIO(txt))
 display(pedigree.individuals)
diff --git a/msprime/pedigrees.py b/msprime/pedigrees.py
index 3a019b8d0..64af1c9d6 100644
--- a/msprime/pedigrees.py
+++ b/msprime/pedigrees.py
@@ -118,7 +118,7 @@ def parse_pedigree(text_file, demography=None, sequence_length=None):
         raise ValueError("Pedigree file must contain at least a header")
     columns = _parse_pedigree_header(header, demography=demography)
     individuals = []
-    tsk_id_map = {"NA": -1}
+    tsk_id_map = {".": -1}
     for tsk_id, row in enumerate(text_file):
         line = tsk_id + 2
         tokens = row.split()
@@ -137,8 +137,8 @@ def parse_pedigree(text_file, demography=None, sequence_length=None):
                     f"on line {line}: {ve}"
                 )
         ind = TextFileIndividual(**kwargs)
-        if ind.id == "NA":
-            raise ValueError(f"'NA' cannot be used as an individual ID (line {line})")
+        if ind.id == ".":
+            raise ValueError(f"'.' cannot be used as an individual ID (line {line})")
         if ind.id in tsk_id_map:
             raise ValueError(f"Duplicate ID at line {line}")
         tsk_id_map[ind.id] = len(individuals)
@@ -193,7 +193,7 @@ def write_pedigree(ts, out):
         parents = []
         for parent in ind.parents:
             if parent == tskit.NULL:
-                parents.append("NA")
+                parents.append(".")
             else:
                 parents.append(parent)
         print(


Test patch:
diff --git a/tests/test_pedigree.py b/tests/test_pedigree.py
index 1db273683..a06936736 100644
--- a/tests/test_pedigree.py
+++ b/tests/test_pedigree.py
@@ -1262,7 +1262,7 @@ class TestParsePedigreeExamples:
     def test_single_all_columns(self):
         text = """\
         # id parent0 parent1 time is_sample
-        X NA NA 0 1
+        X . . 0 1
         """
         tables = parse_indented(text)
         assert len(tables.individuals) == 1
@@ -1275,7 +1275,7 @@ def test_single_all_columns(self):
     def test_single_row_min_columns(self):
         text = """\
         # id parent0 parent1 time
-        X NA NA 0
+        X . . 0
         """
         tables = parse_indented(text)
         assert len(tables.individuals) == 1
@@ -1297,8 +1297,8 @@ def test_trio(self):
         text = """\
         # id parent0 parent1 time
         child mom dad 0
-        mom NA NA 1
-        dad NA NA 1
+        mom . . 1
+        dad . . 1
         """
         tables = parse_indented(text)
         assert len(tables.individuals) == 3
@@ -1313,7 +1313,7 @@ def test_trio(self):
     def test_single_population_name(self):
         text = """\
         # id parent0 parent1 time population
-        A NA NA 0 pop_0
+        A . . 0 pop_0
         """
         demography = msprime.Demography.isolated_model([1])
         tables = parse_indented(text, demography=demography)
@@ -1324,7 +1324,7 @@ def test_single_population_name(self):
     def test_single_population_id(self):
         text = """\
         # id parent0 parent1 time population
-        A NA NA 0 0
+        A . . 0 0
         """
         demography = msprime.Demography.isolated_model([1])
         tables = parse_indented(text, demography=demography)
@@ -1335,7 +1335,7 @@ def test_single_population_id(self):
     def test_two_populations_name(self):
         text = """\
         # id parent0 parent1 time population
-        A NA NA 0 pop_1
+        A . . 0 pop_1
         """
         demography = msprime.Demography.isolated_model([1, 1])
         tables = parse_indented(text, demography=demography)
@@ -1346,7 +1346,7 @@ def test_two_populations_name(self):
     def test_two_populations_id(self):
         text = """\
         # id parent0 parent1 time population
-        A NA NA 0 1
+        A . . 0 1
         """
         demography = msprime.Demography.isolated_model([1, 1])
         tables = parse_indented(text, demography=demography)
@@ -1389,7 +1389,7 @@ def test_parital_row_min(self):
     def test_parital_row_missing_optional(self):
         text = """\
         # id parent0 parent1 time is_sample
-        child NA NA 0
+        child . . 0
         """
         with pytest.raises(ValueError, match="Incorrect number of columns"):
             parse_indented(text)
@@ -1397,8 +1397,8 @@ def test_parital_row_missing_optional(self):
     def test_duplicate_id(self):
         text = """\
         # id parent0 parent1 time
-        child NA NA 0
-        child NA NA 1
+        child . . 0
+        child . . 1
         """
         with pytest.raises(ValueError, match="Duplicate ID"):
             parse_indented(text)
@@ -1406,7 +1406,7 @@ def test_duplicate_id(self):
     def test_missing_id(self):
         text = """\
         # id parent0 parent1 time
-        child missing NA 0
+        child missing . 0
         """
         with pytest.raises(ValueError, match="Parent ID 'missing' not defined"):
             parse_indented(text)
@@ -1414,7 +1414,7 @@ def test_missing_id(self):
     def test_bad_time(self):
         text = """\
         # id parent0 parent1 time
-        child NA NA bad_time
+        child . . bad_time
         """
         with pytest.raises(ValueError, match="value for column 'time' on line 2"):
             parse_indented(text)
@@ -1422,24 +1422,24 @@ def test_bad_time(self):
     def test_bad_is_sample(self):
         text = """\
         # id parent0 parent1 time is_sample
-        child NA NA 0 abcd
+        child . . 0 abcd
         """
         with pytest.raises(ValueError, match="value for column 'is_sample' on line 2"):
             parse_indented(text)
 
-    def test_id_is_NA(self):
+    def test_id_is_dot(self):
         text = """\
         # id parent0 parent1 time
-        NA NA NA 0
+        . . . 0
         """
-        with pytest.raises(ValueError, match="'NA' cannot be used"):
+        with pytest.raises(ValueError, match="'.' cannot be used"):
             parse_indented(text)
 
     def test_line_no(self):
         text = """\
         # id parent0 parent1 time
-        me NA NA 0
-        NA NA NA 0
+        me . . 0
+        . . . 0
         """
         # People (and vim) usually count file lines from 1, so error on line 3
         with pytest.raises(ValueError, match="line 3"):