{"id":322,"date":"2021-06-18T07:00:46","date_gmt":"2021-06-18T12:00:46","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=322"},"modified":"2024-08-17T08:07:59","modified_gmt":"2024-08-17T13:07:59","slug":"palindrome-dates-setad-emordnilap","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2021\/06\/palindrome-dates-setad-emordnilap\/","title":{"rendered":"Palindrome dates : setad emordnilaP"},"content":{"rendered":"<blockquote><p><em>&#8220;What&#8217;s a palindrome again? Wait, I think that&#8217;s an anagram. I can&#8217;t even spell onomatopoeia.&#8221;<\/em><\/p><\/blockquote>\n<p>A palindrome is a word or phrase (or anything, really) that&#8217;s written the same forward and backward. For example:<\/p>\n<ul>\n<li>Racecar<\/li>\n<li>Madam<\/li>\n<li>Evil olive<\/li>\n<li>Step on no pets<\/li>\n<\/ul>\n<p>Or my favorite, <a href=\"https:\/\/en.wiktionary.org\/wiki\/aibohphobia\" target=\"_blank\" rel=\"noopener\">Aibohphobia<\/a>, the fear of palindromes.<\/p>\n<p>We can apply this concept to dates as well. Take the next upcoming palindrome date: February 20, 2022. The digits <code>2 20 2022<\/code> read the same in reverse.<\/p>\n<p>A critical observer might point out that not everyone uses a <em>month-day-year<\/em> date format like us Americans. In fact most of the world outside the United States writes <a href=\"https:\/\/translation-blog.trustedtranslations.com\/how-are-dates-written-in-different-countries-2016-04-08.html\" target=\"_blank\" rel=\"noopener\"><em>day-month-year<\/em><\/a>. Isn&#8217;t the <a href=\"https:\/\/www.timeanddate.com\/calendar\/about-chinese.html\" target=\"_blank\" rel=\"noopener\">Chinese calendar<\/a> on 4000-something? What about leading zeroes and a 2-digit year format?<\/p>\n<p>Those are fair points. I have to admit that palindrome dates are an arbitrary, ultimately meaningless concept. But so are a lot of fun things in life and that&#8217;s never stopped us from enjoying them before.<\/p>\n<p>In this post I&#8217;ll identify all the palindrome dates from now until the year 3000. Then I&#8217;ll plot their frequency and see if any obvious patterns emerge.<\/p>\n<hr \/>\n<p>The general approach will be:<\/p>\n<ol>\n<li>Create a datetime object at January 1, 2000.<\/li>\n<li>Convert it to a string.<\/li>\n<li>Check if it&#8217;s a palindrome. If it is, make a note of it.<\/li>\n<li>Increment one day and check again.<\/li>\n<li>Repeat ad infinitum, or rather until 3000 A.D.<\/li>\n<\/ol>\n<p>Eventually we&#8217;ll group palindrome counts into decade-sized bins. <em>pandas<\/em> makes that part easy. On the other hand <em>pandas<\/em> <code>Timestamp<\/code> objects only count up to the year 2262. Usually that&#8217;s okay but it won&#8217;t be enough today. Instead we&#8217;ll use the <code>datetime<\/code> class from the <code>datetime<\/code> module.<\/p>\n<hr \/>\n<h4>1. Hunt for palindrome dates.<\/h4>\n<p>Begin with the imports.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from datetime import datetime, timedelta\r\nfrom collections import defaultdict\r\nimport pandas as pd\r\nimport matplotlib.pyplot as plt<\/pre>\n<p>The <em>pandas<\/em> <a href=\"https:\/\/pandas.pydata.org\/docs\/reference\/api\/pandas.DataFrame.append.html\" target=\"_blank\" rel=\"noopener\">documentation<\/a> says it&#8217;s more efficient to take whole lists of data and create a dataframe all at once, rather than to append new rows to a dataframe over and over again. We&#8217;ll use a dictionary to keep a running tally of palindrome date occurrences and worry about a dataframe later.<\/p>\n<p>We can use a <code>defaultdict<\/code> from the <em>collections<\/em> module to streamline bookkeeping. This special type of dictionary allows us to reference a key that hasn&#8217;t yet been created. Instead of raising an error when we attempt to reference a non-existent key, it creates one and gives it a default value, in this case zero.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">annual_count = defaultdict(int)<\/pre>\n<p><code>date<\/code> is a counter variable that is incremented until it reaches whatever endpoint we set. Notice how <code>defaultdict<\/code> makes things easy. We don&#8217;t have to check if a year exists in the dictionary before incrementing it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">date = datetime(2000, 1, 1)\r\n\r\nwhile date &lt;= datetime(2999, 12, 31):\r\n    date_string = date.strftime(\"%-m%-d%Y\")\r\n    if date_string == date_string[::-1]:\r\n        year = int(date.strftime(\"%Y\"))\r\n        annual_count[year] += 1\r\n    date += timedelta(days=1)<\/pre>\n<p>Now we have all the data we need. The only problem is that it&#8217;s in an annual form, which would be difficult to visualize or understand. The next step will be to turn yearly counts into decade-long counts. But first let&#8217;s turn the dictionary into a dataframe.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df = pd.DataFrame({\"year\": annual_count.keys(),\r\n                   \"palindromes\": annual_count.values()})<\/pre>\n<p><code>pandas.cut<\/code> is perfect for this task. The process is essentially like preparing a histogram. We define boundaries between each bin (every 10 years) and check where each row belongs. That information goes into a new column.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">bins = range(2000, 3010, 10)\r\nlabels = range(2005, 3005, 10)\r\ndf.loc[:, \"decade\"] = pd.cut(df[\"year\"], bins=bins, labels=labels, right=False)<\/pre>\n<p><code>right<\/code> refers to the upper boundary of each bin. If you&#8217;re familiar with interval notation it looks like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">[left, right)<\/pre>\n<p>We specify labels because without them every cell in the new column would look like, for example, <code>[2000, 2010)<\/code>. I prefer this cell&#8217;s value to be 2005\u2014the middle of the interval. That way when I generate a bar plot the bars will be in the correct place.<\/p>\n<p>At this point <code>df.head(10)<\/code> looks like this. Notice a new <code>decade<\/code> column that defines to which decade each row belongs:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">   year  palindromes decade\r\n0  2011            1   2015\r\n1  2012            1   2015\r\n2  2013            1   2015\r\n3  2014            1   2015\r\n4  2015            1   2015\r\n5  2016            1   2015\r\n6  2017            1   2015\r\n7  2018            1   2015\r\n8  2019            1   2015\r\n9  2021            1   2025<\/pre>\n<p>Use <code>groupby<\/code> to finally generate the information we want: the total number of palindromes in each decade.<\/p>\n<p><code>as_index<\/code> is False so that <code>decade<\/code> doesn&#8217;t become an index. Without this argument <code>df2<\/code> would be a <em>Series<\/em> rather than a <em>DataFrame<\/em>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df2 = df.groupby(\"decade\", as_index=False)[\"palindromes\"].sum()<\/pre>\n<p><code>df2.head(10)<\/code> looks like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">  decade  palindromes\r\n0   2005            0\r\n1   2015            9\r\n2   2025            9\r\n3   2035            8\r\n4   2045            0\r\n5   2055            0\r\n6   2065            0\r\n7   2075            0\r\n8   2085            0\r\n9   2095            0<\/pre>\n<p>This means there are 9 palindrome dates in the 2010&#8217;s, 9 in the 2020&#8217;s, 8 in the 2030&#8217;s, none in the 2040&#8217;s, and so on.<\/p>\n<hr \/>\n<h4>2. Plot the data.<\/h4>\n<p>It&#8217;s time to plot the data. Although I&#8217;d like to use <em>Seaborn<\/em>&#8216;s <code>barplot()<\/code>, it expects categorical data along the x-axis, which would make it difficult to customize x-ticks. Instead I&#8217;ll use <em>Matplotlib<\/em> directly and implement the built-in <code>seaborn<\/code> style to achieve roughly the same appearance.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.style.use(\"seaborn\")\r\nfig, ax = plt.subplots(figsize=(14, 6))\r\nfig.subplots_adjust(left=0.042, right=0.986, top=0.944, bottom=0.096)\r\n\r\nax.bar(df2[\"decade\"], df2[\"palindromes\"], width=8)\r\n\r\nax.set_xticks(range(2000, 3100, 100))\r\nax.set_yticks(range(0, 22, 2))\r\n\r\nax.set_xlim(1975, 3025)\r\nax.set_ylim(0, 20.5)\r\n\r\nfont = \"Ubuntu Condensed\"\r\n\r\nplt.xticks(font=font, fontsize=13)\r\nplt.yticks(font=font, fontsize=13)\r\n\r\nax.set_xlabel(\"Decade\", font=font, size=13, labelpad=6)\r\nax.set_ylabel(\"Count\", font=font, size=13, labelpad=6)\r\nax.set_title(\"Palindrome Dates Per Decade  |  2000-3000 A.D.\", font=font, size=15)\r\n\r\nplt.show()<\/pre>\n<p><strong>The output:<\/strong><\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/06\/palindrome_dates.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-1763 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/06\/palindrome_dates.png\" alt=\"\" width=\"1400\" height=\"600\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/06\/palindrome_dates.png 1400w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/06\/palindrome_dates-300x129.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/06\/palindrome_dates-1024x439.png 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/06\/palindrome_dates-768x329.png 768w\" sizes=\"auto, (max-width: 1400px) 100vw, 1400px\" \/><\/a><\/p>\n<hr \/>\n<h4>3. Discussion.<\/h4>\n<p>The first things I notice are:<\/p>\n<ol>\n<li>The 2200&#8217;s will be a golden age for palindrome dates.<\/li>\n<li>They mostly follow a regular pattern and appear in the first few decades of each century.<\/li>\n<\/ol>\n<p>In total there are 306 palindrome dates during this 1000-year period. If we take a look at the number of digits in each string an interesting pattern emerges:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\">During the 2200s...\r\n6 digits:  81\r\n7 digits:  21\r\n8 digits:   3\r\n\r\nDuring the other 900 years...\r\n6 digits:   0\r\n7 digits: 198\r\n8 digits:   3<\/pre>\n<p>It turns out that the 2200&#8217;s have 81 6-digit palindrome dates and the other 900 years experience none. That&#8217;s because of the double-2 at the beginning of every year, e.g. <span style=\"text-decoration: underline;\">22<\/span>34. It allows for a symmetric date structure that isn&#8217;t possible otherwise.<\/p>\n<p>Take for example June 7, 2276:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\">6 7 2 2 7 6<\/pre>\n<p>It&#8217;s only possible for a 6-digit date string to be palindromic if the middle 2 digits are the same, which only occurs during the 2200&#8217;s.<\/p>\n<p>As you&#8217;d expect, the next millennium will follow a similar pattern with the 3300&#8217;s being the most active palindrome century.<\/p>\n<p>So I plan to enjoy all the palindrome dates I can over the next 18 years, because after September 30, 2039 they won&#8217;t appear again until the year 2101.<\/p>\n<p>Remember to appreciate that you aren&#8217;t a <em>day-month-year<\/em> person. They&#8217;re currently 2 years into an 81-year palindrome drought. Imagine the despair.<\/p>\n<hr \/>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from datetime import datetime, timedelta\r\nfrom collections import defaultdict\r\nimport pandas as pd\r\nimport matplotlib.pyplot as plt\r\n\r\n\r\nannual_count = defaultdict(int)\r\n\r\ndate = datetime(2000, 1, 1)\r\n\r\nwhile date &lt;= datetime(2999, 12, 31):\r\n    date_string = date.strftime(\"%-m%-d%Y\")\r\n    if date_string == date_string[::-1]:\r\n        year = int(date.strftime(\"%Y\"))\r\n        annual_count[year] += 1\r\n    date += timedelta(days=1)\r\n\r\ndf = pd.DataFrame({\"year\": annual_count.keys(), \"palindromes\": annual_count.values()})\r\n\r\nbins = range(2000, 3010, 10)\r\nlabels = range(2005, 3005, 10)\r\ndf.loc[:, \"decade\"] = pd.cut(df[\"year\"], bins=bins, labels=labels, right=False)\r\n\r\ndf2 = df.groupby(\"decade\", as_index=False)[\"palindromes\"].sum()\r\n\r\nplt.style.use(\"seaborn\")\r\nfig, ax = plt.subplots(figsize=(14, 6))\r\nfig.subplots_adjust(left=0.042, right=0.986, top=0.944, bottom=0.096)\r\n\r\nax.bar(df2[\"decade\"], df2[\"palindromes\"], width=8)\r\n\r\nax.set_xticks(range(2000, 3100, 100))\r\nax.set_yticks(range(0, 22, 2))\r\n\r\nax.set_xlim(1975, 3025)\r\nax.set_ylim(0, 20.5)\r\n\r\nfont = \"Ubuntu Condensed\"\r\n\r\nplt.xticks(font=font, fontsize=13)\r\nplt.yticks(font=font, fontsize=13)\r\n\r\nax.set_xlabel(\"Decade\", font=font, size=13, labelpad=6)\r\nax.set_ylabel(\"Count\", font=font, size=13, labelpad=6)\r\nax.set_title(\"Palindrome Dates Per Decade  |  2000-3000 A.D.\", font=font, size=15)\r\n\r\nplt.show()<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>&#8220;What&#8217;s a palindrome again? Wait, I think that&#8217;s an anagram. I can&#8217;t even spell onomatopoeia.&#8221; A palindrome is a word or phrase (or anything, really) that&#8217;s written the same forward and backward. For example: Racecar Madam Evil olive Step on<\/p>\n","protected":false},"author":1,"featured_media":1658,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[40],"tags":[55,52,53,56,38,24,57,58,30,25,36,54],"class_list":["post-322","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-math","tag-cut","tag-date","tag-datetime","tag-groupby","tag-histogram","tag-matplotlib","tag-palindrome","tag-palindrome-date","tag-pandas","tag-python","tag-seaborn","tag-timestamp"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/322","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/comments?post=322"}],"version-history":[{"count":39,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/322\/revisions"}],"predecessor-version":[{"id":1764,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/322\/revisions\/1764"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/1658"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=322"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=322"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=322"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}