<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Moon river]]></title><description><![CDATA[My huckleberry friend, moon river, and me.]]></description><link>https://blog.ruosilin.com/</link><image><url>https://blog.ruosilin.com/favicon.png</url><title>Moon river</title><link>https://blog.ruosilin.com/</link></image><generator>Ghost 5.26</generator><lastBuildDate>Sat, 04 Jan 2025 02:18:27 GMT</lastBuildDate><atom:link href="https://blog.ruosilin.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[New Domain!]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I love my .me domain, but it&apos;s getting expensive and I received so many unwanted emails. So effective today, my new domain for the blog would be <a href="blog.ruosilin.com">blog.ruosilin.com</a>! It&apos;s been a while since I set up a new domain, but it&apos;s not</p>]]></description><link>https://blog.ruosilin.com/new-domain/</link><guid isPermaLink="false">6445e9490572a05f49758f8b</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Mon, 24 Apr 2023 02:31:19 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>I love my .me domain, but it&apos;s getting expensive and I received so many unwanted emails. So effective today, my new domain for the blog would be <a href="blog.ruosilin.com">blog.ruosilin.com</a>! It&apos;s been a while since I set up a new domain, but it&apos;s not hard - I have Cloudflare to take care of DNS, certbot for HTTPS redirect, and (of course! always fun) Ghost set up to point to the new subdomain. In fact I didn&apos;t anticipate a .com domain, my original plan was to get a .ds but it&apos;s handshake only (in other words: active in the crypto world). Either way I&apos;m glad with the new domain and would love to share more here!!</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Hello from Ghost 5.x]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>It took me ~5 hours to finally take a screenshot of this</p>
<p><img src="https://blog.ruosilin.com/content/images/2022/12/Capture-1.PNG" alt="Capture-1" loading="lazy"></p>
<p>A couple of notes (mainly for myself to refer to in the future):</p>
<ul>
<li>It looks like Ghost prefers to have node upgraded through NodeSource Node.js Binary Distributions (<a href="https://ghost.org/docs/faq/node-versions/">source1</a>, <a href="https://sandervoogt.com/how-to-fix-ghost-start-not-working/">source2</a>), but <code>nvm</code> works. Just make sure you get</li></ul>]]></description><link>https://blog.ruosilin.com/hello-from-ghost-5-x/</link><guid isPermaLink="false">63a72cff79c18864caa271f5</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Sat, 24 Dec 2022 17:09:15 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>It took me ~5 hours to finally take a screenshot of this</p>
<p><img src="https://blog.ruosilin.com/content/images/2022/12/Capture-1.PNG" alt="Capture-1" loading="lazy"></p>
<p>A couple of notes (mainly for myself to refer to in the future):</p>
<ul>
<li>It looks like Ghost prefers to have node upgraded through NodeSource Node.js Binary Distributions (<a href="https://ghost.org/docs/faq/node-versions/">source1</a>, <a href="https://sandervoogt.com/how-to-fix-ghost-start-not-working/">source2</a>), but <code>nvm</code> works. Just make sure you get <code>16.13.0</code> (I thought <code>16.x</code> would work generally, however <code>16.0.0</code> failed, so had to pin to the exact version)</li>
<li>Make sure that <code>ExecStart</code> in your systemd service file points to the current node version (which can be found through <code>whereis node</code>). Also after updating, <code>cp</code> the file to the systemd folder (or create a symlink... I have no idea why the one under my <code>&lt;blog root&gt;/system/files</code> is not symlink to the one under <code>/etc/systemd/system/</code>) and run <code>systemctl daemon-reload</code> so that changes may take effect.</li>
<li>Somehow my production config has <code>database = mysql2</code>... if running into error you&apos;ll need to manually run <code>ghost config set database.client mysql</code> (this command will show up if you have config error when running <code>ghost start</code>)</li>
<li>It&apos;s possible (so far) to upgrade to MySQL 8 on Ubuntu 16.04, just remember to back up all the data. <code>mysqldump -u root -p --all-databases &gt; alldb.sql</code> (<a href="https://stackoverflow.com/a/26096339">source</a>)</li>
<li>If <code>ghost start</code> keeps exiting with errors, try to run <code>ghost run</code> first and examine the console. You may see errors like <code>WARN Can&apos;t connect to the bootstrap socket (localhost 8000) ECONNREFUSED</code> but that&apos;s okay, the CLI will attempt 3 times and eventually give up. When starting as normal the production config file <code>config.production.json</code> should NOT have any bootstrap socket related info.</li>
<li>Reading the error logs definitely helps... <code>journalctl -u your_ghost_service -n 50</code> and fix accordingly...</li>
</ul>
<p>In the short run I don&apos;t anticipate another major update soon, unless another security vulnerability is exposed. Or maybe, as <a href="https://wzyboy.im/post/1092.html">wzyboy</a> has suggested... time to move to Lektor?</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Migration from Twitter to Fediverse]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Well, it&apos;s been a year since I last posted here. I feel that I&apos;ve easily lost track of time as I grow older, and by googling I found a <a href="https://studyfinds.org/why-time-flies-as-we-get-older/#:~:text=Now%2C%20a%20fascinating%20study%20offers,of%20time%20to%20speed%20up.">plausible explanation</a>. ChatGPT also provides another perspective, which is not entirely based on science but does offer</p>]]></description><link>https://blog.ruosilin.com/migration-to-fediverse/</link><guid isPermaLink="false">63a6048318f35e0e609aad42</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Fri, 23 Dec 2022 20:39:46 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>Well, it&apos;s been a year since I last posted here. I feel that I&apos;ve easily lost track of time as I grow older, and by googling I found a <a href="https://studyfinds.org/why-time-flies-as-we-get-older/#:~:text=Now%2C%20a%20fascinating%20study%20offers,of%20time%20to%20speed%20up.">plausible explanation</a>. ChatGPT also provides another perspective, which is not entirely based on science but does offer a sense of comfort as well:</p>
<p><img src="https://blog.ruosilin.com/content/images/2022/12/Capture.PNG" alt="Capture" loading="lazy"></p>
<p>Back to the topic - though the whole year felt like a blink of an eye, a lot of things happened. Not only to me (on my personal level there are indeed some achievements &amp; lessons I took notes of, maybe will share in another post?) but also to the world, and an important one is the recent <a href="https://en.wikipedia.org/wiki/Acquisition_of_Twitter_by_Elon_Musk">Twitter fiasco</a>. I first joined Twitter in October 2008 (fun fact: President Obama was one of my followers), and have been an active user since 2011 (one of the DAU for sure). To me, Twitter is not only a platform for information but also a place full of my digital life and memories. From there I met a handful of amazing people, and we even had some offline face time together which leads to everlasting relationships in the real world. I was able to reflect, relax and rewind. I picked up trendy topics and had in-depth conversations with avid practitioners in the same space, which is impossible given the physical boundaries among different continents. So Twitter has been, and is always special, in my mind. As a regular user, I don&apos;t want to talk too much about the recent policy changes since it becomes private. What motivates me to leave, though, is the following (looks like this act has been reversed, but still the scar is fresh in my mind):</p>
<p><img src="https://blog.ruosilin.com/content/images/2022/12/FkR1CyoX0AANLMc.jpg" alt="FkR1CyoX0AANLMc" loading="lazy"></p>
<p>Since the <a href="https://www.reuters.com/technology/twitter-exec-says-50-employees-lost-jobs-following-acquisition-2022-11-04/">bloody layoff</a> in early November, I&apos;ve noticed <code>Mastodon</code> being mentioned a lot on my timeline. A quick search educates me that it is a decentralized, open-sourced social network that belongs to the greater Fediverse framework. I&apos;d like to avoid using <code>Mastodon</code> to represent <code>Fediverse</code> though, just like you can&apos;t use Toyota as the synonym for the whole consumer automobile industry. Mastodon, though, is the most known, biggest Fediverse software implementation out there. I also have an account under the <code>mastodon.world</code> server, which I have no plan to stay forever as eventually I&apos;ll self-host an instance powered by <a href="https://pleroma.social/">Pleroma</a>. For those who are still hesitant to make the move from Twitter to Fediverse, though, I hope to provide some tricks and observations so that your transition will be smooth as well.</p>
<h2 id="useful-tools">Useful Tools</h2>
<ul>
<li><a href="https://instances.social/">Mastodon instances</a>: this site helps me quickly navigate and narrow down a list of candidate Mastodon servers to join. I like the <code>Advanced list</code> which allows me to rank servers by uptime, obs, and users. Rule of thumb: join the ones that are large enough with high uptime, so that you&apos;ll have the best user experience. Or maybe join your friends&apos; servers, as word of mouth is the most basic but reliable source.</li>
<li><a href="https://fedifinder.glitch.me/">Fedifinder</a>: this tool can scrape any Fediverse account published on Twitter from your following list. I didn&apos;t read the source code so unsure how it works, but I suspect it scans through your following&apos;s bio, user name and possibly the top 5 tweets to look for any known Fediverse instance link. We don&apos;t always have to use machine learning, simple pattern matching has an edge too :) I was able to follow ~85 friends in batch thanks to this tool.</li>
<li><a href="https://github.com/klausi/mastodon-twitter-sync">Mastodon Twitter Sync</a>: this package can automatically sync your toots to Twitter, and vice versa. Note: quote retweets on Twitter will be synchronized to Fediverse despite <code>sync_retweets</code> = False, that&apos;s because quote retweets != retweets. Also when synchronizing reblogs to Twitter, the original author name does not contain any server information. But no other complaints since it&apos;s a great free tool! It can also link your toots/tweets with your self-replies. Setting up using the Docker build is easy, but remember to take out the <code>-t</code> flag when running it in your crontab... my mailbox was bombarded with the error <code>the input device is not a TTY</code> when first set to run every 5 minutes...</li>
<li><a href="https://tooot.app/">Tooot: a Mastodon mobile client</a>: it simply works with an elegant design. Though the main focus is for Chinese users, browsing toots in other languages (en &amp; CJK based) is seamless. Sometimes, though, fetching the updates can be slowed. I wonder if it&apos;s an issue related to my server, though.</li>
</ul>
<h2 id="observations">Observations</h2>
<ul>
<li>You don&apos;t have to follow users from the same server; as long as your server is not on the blocklist of another server, you should be able to search for the other user&apos;s handler (e.g. <code>@chrisalbon@sigmoid.social</code>) and follow him/her. If this is the first time that the other user is being followed by someone on your server, it will take time to fetch his/her timeline. Otherwise you should see a couple of toots already present on your side.</li>
<li>Refreshing timelines can be a frustrating experience compared with Twitter, since my server seems to be popular with only limited resources. So, support the server owner in whatever means, if you can! The latency is a bit high but bearable. I plan to opt to self-host <a href="https://pleroma.social/">Pleroma</a> mainly due to it being lightweight, and I&apos;d like to have more control over my data just as how I decided to open up a self-host blog like this.</li>
<li>The 140-character limit no longer applies here! However I lost the ability to express long, consistent thoughts on a social platform... I&apos;ll pick it up gradually :)</li>
<li>You cannot quote &amp; reply to a toot on Mastodon. Looks like this is <a href="https://fedi.tips/how-to-use-mastodon-and-the-fediverse-basic-tips/#WhyCantIQuoteOtherPostsInMastodon">a deliberate design choice</a> but I&apos;ll much appreciate it. There is a workaround, though. You can always manually copy the link to a toot and paste it at the end of your toot, just like how you&apos;d comment on any link on the web. It&apos;s just... blindly inconvenient :)</li>
</ul>
<p>I probably will come back to this post to add more observations/technical details as my time on Mastodon/in Fediverse progresses. So stay tuned! In the meantime, happy holidays! Can&apos;t wait to see how 2023 will unfold for us.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Quick Note on AWS Package Installation under Company Proxy]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Oh wow. I can&apos;t believe I haven&apos;t posted in the entire 2021! It&apos;s been quite a ride. Ever since the pandemic started, time seems to be stagnant for me. Though I&apos;ve been out a bit more than 2020, my mind was still</p>]]></description><link>https://blog.ruosilin.com/what-a-year/</link><guid isPermaLink="false">61b4093e02ad290af34272d2</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Sat, 11 Dec 2021 02:37:06 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>Oh wow. I can&apos;t believe I haven&apos;t posted in the entire 2021! It&apos;s been quite a ride. Ever since the pandemic started, time seems to be stagnant for me. Though I&apos;ve been out a bit more than 2020, my mind was still in 2019 when talking about the last trip (well, technically it should be in October when I visited Seattle. Perfect timing as I didn&apos;t get to experience fall anymore since relocating to Texas). I just wrapped up another project with an intern this semester, which was related to transfer learning on BERT. Though not writing the codes myself, I always feel that I learn more when mentoring an intern (partly due to my role as a resource provider, I ought to know more). There will be another post to talk about BERT and what we learned, but this one would be short &amp; sweet. (AWS is GREAT &#x1F600;)</p>
<p>If you&apos;re using the public version of AWS/no complicated corporate setup, then this post is not for you. But for us, since we take customer data seriously, there are a lot of restrictions. I feel that my hands are tied due to limited access my role provided (<code>AccessDenied</code>, yeah it&apos;s you again). On top of it, due to internal proxy setup, we are not allowed to run simple requests such as:</p>
<blockquote>
<p><code>nltk.download(&apos;punkt&apos;)</code></p>
</blockquote>
<p>Bribe IT and ask the admins to reconfigure my network setup might be a solution, but again this violates the company rule :) Since we had to use basic NLP packages to process texts further on customized AWS, I finally was able to find a way (using NLTK as an example):</p>
<ol>
<li>Download <a href="https://www.nltk.org/data.html">the NLTK data</a> from the company-approved mirror site (If it&apos;s not available in your internal repositories, talk to IT/the open-source team again).</li>
<li>Upload the package (in my case, it&apos;s <code>gh-pages.zip</code>) to S3.</li>
<li>In your SageMaker notebook instance, download the package from S3 (make sure that the IAM policy was properly set up - in other words, SageMaker can connect to S3):</li>
</ol>
<pre><code># source: https://gist.github.com/mikhailklassen/de3da3584c45cedb5b0df7feaead6b1f#file-download_file_s3_sagemaker-py
# AWS Python SDK
import boto3

# When running on SageMaker, need execution role
from sagemaker import get_execution_role
role = get_execution_role()

# Declare bucket name, remote file, and destination
my_bucket = &apos;s3-bucket-name&apos;
orig_file = &apos;full/path/to/file.p&apos;
dest_file = &apos;local/path/to/file.p&apos;

# Connect to S3 bucket and download file
s3 = boto3.resource(&apos;s3&apos;)
s3.Bucket(my_bucket).download_file(orig_file, dest_file)
</code></pre>
<ol start="4">
<li>Once the files are transmitted to your SageMaker notebook container, unzip it: <code>unzip gh-pages.zip -d /path/to/directory</code></li>
<li>Load the file manually:</li>
</ol>
<pre><code>import nltk
nltk.data.path.append(&quot;/path/to/directory&quot;)
</code></pre>
<p><code>punkt</code> should then be able to use. You may need to be careful on the append path to make sure that it&apos;s one level above <code>punkt</code>, also the package should be already unzipped.</p>
<p>I doubt if the trick I provided here would ever be useful for anyone on the public web. My lesson learned here is: to test things in a constrained SageMaker notebook container, better upload the files to S3 first and then download them from there. Direct upload to SageMaker notebook instance will fail miserably (partial data transmitted, plus the speed is way too slow).</p>
<!--kg-card-end: markdown--><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Causal Inference: Basics & My Thoughts]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Well, technically I didn&apos;t apply the technique to solve a problem myself - as I served as a mentor in this case. Long story short, almost a year ago I was assigned to a project which aimed to identify top attributes associated with a target. Eventually, we found</p>]]></description><link>https://blog.ruosilin.com/my-experience-with-causal-inference/</link><guid isPermaLink="false">5fb46a9ce4681b0a8762d458</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Wed, 02 Dec 2020 02:05:15 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>Well, technically I didn&apos;t apply the technique to solve a problem myself - as I served as a mentor in this case. Long story short, almost a year ago I was assigned to a project which aimed to identify top attributes associated with a target. Eventually, we found a handful of variables, and our client would like to drill down specifically on one of them. Based on the sample data, I checked the association between that variable and the target, and the results were inconclusive. The project was then put on hold, but it lingered in my mind. I then learned that there is an active research field called <a href="https://en.wikipedia.org/wiki/Causal_inference">Causal Inference</a> which seems to provide systematic ways to answer the core question that my client has: that is, if we intervene on variable <code>X</code>, will it cause changes on the outcome <code>Y</code> (regardless of positive or negative)? Causal inference sounds like a perfect tool to address this kind of question, therefore I framed a research question and requested an intern to work on it. It turns out to be a bit challenging than I originally thought, though. I&apos;ll try to summarize the basics we&apos;ve covered for this project, which I believe are the stepping stones for advanced topics in this field. Hopefully, I&apos;ll be able to finish grasping the main ideas from <a href="https://mitpress.mit.edu/books/elements-causal-inference">ECI</a> then!</p>
<h2 id="sowhatiscausalinference">So, What Is Causal Inference?</h2>
<p>Per <a href="https://en.wikipedia.org/wiki/Causal_inference">Wikipedia</a>, causal inference...</p>
<blockquote>
<p>is the process of drawing a conclusion about a causal connection based on the conditions of the occurrence of an effect.</p>
</blockquote>
<p>It could be described in many different ways under various contexts, but to me, it means: can we say that treatment causes a change in the outcome?</p>
<ul>
<li>Treatment: the intervention that we take.
<ul>
<li>For instance, whether arranging heart transplant for a patient or not; whether changing the wording of a homepage or not. These treatments are binary.</li>
<li>If there are multiple treatments within a timeframe (e.g. yearly follow-up with AIDS patients over 5 years, each time providing a different dose of medicine) that will be considered as time-varied treatments, which I have no exposure so far (the <a href="https://www.hsph.harvard.edu/miguel-hernan/causal-inference-book/">Causal Inference book</a> dedicates a whole section for it, and I found it quite difficult to follow).</li>
<li>I wonder if it&apos;s possible to have <em>continuous</em> treatments... would be quite hard to do so, especially in an experimental setting? Multi-level discrete treatments are more likely, but fortunately, as my first project involving causal inference, I just need to deal with binary treatment.</li>
</ul>
</li>
<li>Outcome: the end result of a study/an experiment/something that we are measuring.
<ul>
<li>For the 2 examples that I&apos;ve shown above, their outcomes could both be &quot;success&quot; (whether a patient survived or not; increase in revenue/CTR).</li>
<li>Outcome could be discrete (Yes/No) or continuous (e.g. change in revenue).</li>
</ul>
</li>
</ul>
<h2 id="randomizedexperimentsvsobservationalstudy">Randomized Experiments vs. Observational Study</h2>
<p>If a causal analysis is performed in a <em>control experiment setting</em>, it would be fantastic. I&apos;m not an expert in experimental designs, but randomized experiments really help (also make it easier to check the 3 conditions above). In reality, though, it could be that we are simply collecting a snapshot of data retrospectively. These data would be considered as <em>observational data</em>, as they were passively captured. My impression is that if one would like to draw a conclusion using observational study data, he/she should gather a large enough dataset (here comes another question: what do I mean by &quot;large enough&quot;? Well, it depends) with a handful of measured variables. Even then, causal inference is built upon a series of assumptions, so clearly stating the assumptions and limitations of the result is crucial.</p>
<h2 id="identifiabilityconditions">Identifiability Conditions</h2>
<p>The following 3 conditions should be checked prior to conduct a causal analysis:</p>
<ul>
<li>Exchangeability: each subpopulation within different stratums should be exchangeable. For instance, had I prescribed this drug to patients who do not receive it at the moment, their responses should be the same as those who currently get the drug.</li>
<li>Positivity: each subpopulation should have at least 1 instance. Say that I don&apos;t have any male samples that live in Boston, age &gt; 50 with &gt; 10K annual income and 2 dogs, my study would fail to meet the positivity assumption.</li>
<li>Consistency: the observed outcome should be the same as the counterfactual outcome for every treated/untreated sample. Let&apos;s assume you notice that taking Aspirin will noticeably reduce the risk of heart attack by 5%. This should be consistent across the board - regardless of the brand.
<ul>
<li>I personally find the idea of consistency a bit abstract to understand/explain. The &quot;Causal Inference: What If&quot; book mainly refers to different versions of treatment (e.g. physicians have their own preferences on conducting the same operation), but I still feel that something is missing, at least in my understanding. If you could find a better example to illustrate, let me know!</li>
</ul>
</li>
</ul>
<p>If dealing with observational data, I doubt if the 3 conditions could all be satisfied flawlessly. My approach, for now, is to consider them first when dealing with a causal problem, and attempt to remedy any missing pieces; if not, I will highlight the failing ones and clearly emphasize them as part of the limitations of this work.</p>
<h2 id="causaldiagram">Causal Diagram</h2>
<p>It typically looks like this:</p>
<p><img src="https://blog.ruosilin.com/content/images/2020/11/Capture.PNG" alt="Causal_diagram" loading="lazy"><br>
<em>Image source: Causal Inference: What If, Miguel A. Hern&#xE1;n, James M. Robins, February 21, 2020, pp. 69</em></p>
<p>Per <a href="https://en.wikipedia.org/wiki/Causal_model">Wikipedia</a>, &quot;A causal diagram is a directed graph that displays causal relationships between variables in a causal model&quot;. I find it useful to sort out variable relationships, especially when you have a handful of them to analyze. The one displays above shows a simple scenario: that is, we have a set of confounders <code>L</code> that affect both the treatment <code>A</code> and the outcome <code>Y</code>. <code>Y</code> is also impacted by <code>A</code>. Causal diagrams are capable of conveying more information, such as blocking, colliders, etc.</p>
<p>I didn&apos;t get to play with the <a href="https://github.com/microsoft/dowhy">dowhy</a> package from Microsoft for this project, which requires a causal diagram as part of the input. I perfer to translate the problem into several causal graphs after verifying the identifiability conditions and finishing exploratory data analysis on core variables.</p>
<h2 id="confounderseffectmodifiers">Confounders &amp; Effect Modifiers</h2>
<p>The two concepts seem similar but indeed they&apos;re not. My take on both:</p>
<ul>
<li>Confounders are <u>variables that affect both the treatment and the outcome</u>. Let&apos;s say that I&apos;m interested in this newly approved medicine that claims to reduce pain by 25%. One measured factor could be pregnancy (0/1). If a patient is pregnant, I might play safe and adjust the treatment doses as compared with non-pregnant subjects; pregnancy might also impact the pain level that a patient could tolerate, thus affecting the outcome too.
<ul>
<li>Confounders need to be adjusted so that we could achieve marginal exchangeability: that is, switching subjects in each subpopulation should achieve the same result. If not adjusting, the conclusion will be biased, as we no longer can say that the treatment is the only variable that causes changes in the outcome (in the graphical representation, that would be a &quot;backdoor path&quot;).</li>
</ul>
</li>
<li>The formal definition of effect modifiers is: <u>an effect modifier <code>V</code> modifies the effect of the treatment <code>A</code> on the outcome <code>Y</code>, when the average causal effect of <code>A</code> on <code>Y</code> varies across levels of <code>V</code></u>. If we find that women are more likely to survive after a heart transplant than men, then gender would be considered as an effect modifier.
<ul>
<li>I had an in-depth discussion under the comment section of <a href="https://www.youtube.com/watch?v=3L468QctWO4">this video</a>. Back then my mindset was wrong - I thought that effect modifiers == variables that only impact the outcome. That is not correct as I didn&apos;t take the treatment into consideration. Variables that only impact the outcome but not the treatment could be excluded from causal diagrams too, as they won&apos;t create loops.</li>
</ul>
</li>
<li>Effect modifiers could be confounders; back to the previous example - if we took gender into consideration when assigning hearts to patients (e.g. female are 30% more likely than male to receive a heart) then it has an impact on the treatment too, therefore it will be a confounder. They could be non-confounders as well if the treatment procedure does not involve them.</li>
<li>Confounders do not have to be effect modifiers. Say that in the heart transplant experiment, patients who are over age 50 are 30% more likely to receive hearts than those under age 50 (i.e. age &gt; 50 affecting the treatment); elderly people may have a shorter life expectation (i.e. age &gt; 50 affecting the outcome). However, in this study, we do not see a change in average causal effects in the over- and under-50 age groups. If so, age &gt; 50 will NOT be an effect modifier, but still a confounder.</li>
<li>Rule of thumbs: adjusting confounders whenever you see one (regardless of effect modification or not). Effect modifiers, if exist, worth reporting too. But you may need to take some time understanding their meanings prior to sharing them with key stakeholders.</li>
</ul>
<h2 id="gmethods">G-methods</h2>
<p>G-methods are a collection of techniques to understand generalized treatment contrasts involving treatments that vary over time. It includes IP Weighting, standardization, and g-estimation. For this work, we applied <em>IP Weighting &amp; standardization</em> to a treatment variable that does not evolve over time (i.e. the treatment was given once and done). I will summarize my understanding and key steps for both algorithms below, but they may be incomplete in the time-varied treatment case.</p>
<h3 id="ipweighting">IP Weighting</h3>
<p>IP weighting is a mechanism that removes the dependency of the treatment <code>A</code> on covariates <code>L</code>. It is able to do so by creating &quot;pseudo-populations&quot;. For each covariate group and each treatment combination, there will be some responses for the outcome group. IP weighting essentially takes the binary tree and splits it into two parts, one with all untreated and the other with all treated. The distributions of <code>L</code> are thus the same in both groups. I find this concept hard to grasp. If you are dealing with binary, one-time <code>A</code> and a set of <code>L</code>, one parametric way to achieve IP weighting is:</p>
<ol>
<li>Estimate the propensity score model, i.e. <code>P(A=1|L)</code>. My suggestion is to start with well-understood linear models first, e.g. logistic regression.</li>
<li>Estimate weights using output probabilities from the propensity score model. For samples whose <code>A</code> = 1, their weights would be <code>1/p</code>. Those that have <code>A</code> = 0 would have their weights equal to <code>1/(1-p)</code>.</li>
<li>Use the weights above to estimate the outcome model, i.e. <code>P(Y|A)</code>. If using generalized linear models (GLM), you may perform weighted least squares and interpret the coefficient associated with the treatment variable.</li>
</ol>
<p><a href="https://www.stat.berkeley.edu/~census/weight.pdf">This paper</a> suggests that one may try truncating weights. The &quot;truncation&quot; does not mean throwing away data; rather, it&apos;s a method to adjust for weird cases, such as samples that are supposed to have a high likelihood of receiving treatment (<code>P(A=1|L)</code> is high) but ended up not getting treatment (<code>A</code>=0), and vice versa. Or you may compare the weighted average responses over the treatment and the control group (should be equivalent to the coefficient obtained from 3. above). I personally value IP weighting over <a href="https://dimewiki.worldbank.org/wiki/Propensity_Score_Matching">Propensity Score Matching</a>.</p>
<h3 id="standardization">Standardization</h3>
<p>Unlike IP weighting, which approaches from the treatment side, standardization approaches from the outcome <code>Y</code> side. The formula looks like below:</p>
<p><img src="https://blog.ruosilin.com/content/images/2020/11/Capture-1.PNG" alt="standardization" loading="lazy"><br>
<em>Image source: Causal Inference: What If, Miguel A. Hern&#xE1;n, James M. Robins, February 21, 2020, pp. 162</em></p>
<p>I like to think of it as a form of weighted average: we obtain conditional expectations per each treatment (<code>A</code> = 0 or 1) &amp; covariates (<code>L</code>) combinations, then marginalize over <code>L</code> (i.e. &quot;standardizing&quot; the expectations). We will be comparing two expectation values here (for binary treatments), one with <code>A</code> = 1 and the other with <code>A</code> = 0. The causal effect estimate will be <code>E(Y|A=1, L=l) - E(Y|A=0, L=l)</code>.</p>
<p>As for implementation, the <a href="https://github.com/jrfiedler/causal_inference_python_code/blob/master/chapter13.ipynb">code</a> accompanying Hern&#xE1;n &amp; Robins&apos;s book summarizes it nicely:</p>
<blockquote>
<ol>
<li>outcome modeling, on the original data</li>
<li>prediction on the expanded dataset</li>
<li>standardization by averaging</li>
</ol>
</blockquote>
<p>Standardization and IP weighting are mathematically equivalent, so they should produce results that agree with each other. If not, something is off (so-called model misspecification).</p>
<h2 id="thingsiwishtoknowmore">Things I Wish to Know More</h2>
<p>I rushed through the first two parts of <a href="https://www.hsph.harvard.edu/miguel-hernan/causal-inference-book/">Causal Inference: What If</a> in less than 3 weeks, so as to finish up the project scoping on time. There are terms that I ran into but didn&apos;t dig through. Some of those include <em>doubly robusted method</em> (a parametric model that is said to work with incorrectly specified treatment or outcome model), <em>sufficient causes</em> (how is the graphical representation relates to causal diagrams?), and <em>instrumental variable estimation</em> (the authors do not sound like a fan to this concept, but I wonder how I may find an ideal set of instrument variables to estimate from another angle). Moreover, I&apos;m interested in incorporating machine learning (e.g. tree-based ensembled models) into causal inference (such as using random forest to estimate the conditional mean, <code>E(Y|A)</code>). I believe this is technically doable, just unsure of the implications and interpretations (time to revisit <a href="https://mitpress.mit.edu/books/elements-causal-inference">ECI</a>?). Another general item is to build a knowledge map - at least in my mind. Now I feel that my knowledge is here and there; though I&apos;m aware of certain terms and concepts, how are they connected? Therefore, I&apos;m taking a step back to read Pearl&apos;s <a href="https://www.wiley.com/en-us/Causal+Inference+in+Statistics%3A+A+Primer-p-9781119186847">classic premier</a>. Brady Neal has put together a fascinating <a href="https://www.bradyneal.com/which-causal-inference-book">flowchart</a> on what book to read, so if you&apos;re new to the field too, check it out!</p>
<h2 id="end">End</h2>
<p>Ah, it took me more than a week or so to finish this blog post! I didn&apos;t proof-read though, so please bear with me for grammatical mistakes. For conceptual misalignments, leave me a message &amp; I&apos;ll be more than willing to investigate.</p>
<p>I normally write technical notes after going through major challenges, so this blog post is no exception. My original goal is to lower the entry barrier of this field - it was painful to go through the literature as a complete novice. If anyone finds this interesting/helpful, that&apos;s a great comfort to me. I might write another blog post to share my journey in causal inference (if only... I have the time and not procrastinating), so stay tuned :)</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[A (series of) Mysterious Flask Error(s)]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>The company-wise hack day for 2020 is tomorrow. It&apos;s my first time participating, so I&apos;m a bit excited. We have an awesome idea to implement, which requires a simple Flask web app. Flashback to late 2018, I did write a simple Flask app by following <a href="https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world">this</a></p>]]></description><link>https://blog.ruosilin.com/a-mysterious-flask/</link><guid isPermaLink="false">5f0df7a558c8c90b0b109965</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Tue, 14 Jul 2020 18:25:23 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>The company-wise hack day for 2020 is tomorrow. It&apos;s my first time participating, so I&apos;m a bit excited. We have an awesome idea to implement, which requires a simple Flask web app. Flashback to late 2018, I did write a simple Flask app by following <a href="https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world">this tutorial</a> so that my hubby (or other interested parties) may upload cat photos to my external storage on Cloudinary. It worked, but uploading multiple photos always timed out. (Heroku seems to allow for max. 30 seconds idled time) Now that I contemplate this, I realize that I could have handled it through JS <code>async</code> call. But that&apos;s another story. In short, I&apos;d like to reuse some legacy Flask codes that I somehow wrote in Python 2.7 (weird, right? Already late 2018, and the tutorial is based on Python 3, but I was still attached to Python 2?) as a boilerplate for my new project. I didn&apos;t imagine it would be that hard, though.</p>
<h2 id="importwithinthesamefoldernolongerworks">Import within the same folder no longer works?</h2>
<p>Below is a snapshot of my folder structure:</p>
<pre><code>+-- app
|   +-- __init__.py
|   +-- config.py
|   +-- ... (Other py files)
+-- migrations
|   +-- (Auto migration files created by SQLAlchemy)
+-- myapp.py
+-- requirements.txt
</code></pre>
<p>Within <code>__init__.py</code>, I have <code>from config import Config</code>. <code>config.py</code> is the configuration file with a <code>Config</code> class object. That&apos;s what the tutorial has, and works perfectly fine with Python 2.7.</p>
<p>After installing all the required packages, I specified the Flask app by setting <code>export FLASK_APP=myapp.py</code>, followed by <code>flask run</code>. The following error occurred:</p>
<pre><code>File &quot;.../app/__init__.py&quot;, line 7, in &lt;module&gt;
  from config import Config
ModuleNotFoundError: No module named &apos;config&apos;
</code></pre>
<p>That&apos;s weird. I have a <code>config.py</code> resided in the same folder with <code>__init__.py</code>! How come it&apos;s not found?</p>
<h2 id="googledsolution1outofsightoutofmind">Googled Solution 1: Out of sight, out of mind</h2>
<p>The first search term that occurred in my mind was a relative import. (Spoiler alert: this turned out to be NOT related to relative import.) I quickly googled and found a <a href="https://www.digitalocean.com/community/questions/python-can-t-find-local-files-modules">solution</a>:</p>
<blockquote>
<p>If your config.py file and init.py file on the same path, move config.py file outside of the path and place the same in root.<br>
for example if both are in &#x201C;APP&#x201D; directory, move the config.py and place it inside the root director of &#x201C;APP&#x201D; directory</p>
</blockquote>
<p>Wow, sounds like exactly what I&apos;ve been going through so far! Can&apos;t wait to try it!</p>
<p>After moving the <code>config.py</code> to the parent folder (aka. not within the <code>app</code> folder), I tried to invoke <code>flask run</code> again. This time, I got similar errors from importing other python files within the same <code>app</code> folder for the <code>__init__.py</code> file. There is no way I could move all files to the parent folder! Something is bound to be wrong in this case.</p>
<h2 id="googledsolution2addsystempath">Googled Solution 2: Add system path</h2>
<p>I dig a little deeper on Python 3 relative import and believed that <a href="https://stackoverflow.com/a/44230992">this</a> should be my cure:</p>
<blockquote>
<p>I had a similar problem, I solved it by explicitly adding the file&apos;s directory to the path list:</p>
</blockquote>
<pre><code>import os
import sys

file_dir = os.path.dirname(__file__)
sys.path.append(file_dir)
</code></pre>
<blockquote>
<p>After that, I had no problem importing from the same directory.</p>
</blockquote>
<p>I copied and pasted the code to <code>__init__.py</code> and voila! Not only <code>config.py</code> could be seen, but also other files under the same <code>app</code> folder!</p>
<h2 id="butwhataboutthelocaldatabase">... But what about the local database?</h2>
<p>The app has a SQLite local database that contains only one table to store registered user information. I was still not able to run the app due to the following error:</p>
<pre><code>sqlalchemy.exc.InvalidRequestError: Table &apos;user&apos; is already defined for this MetaData instance.  Specify &apos;extend_existing=True&apos; to redefine options and columns on an existing Table object.
</code></pre>
<p>Per <a href="https://github.com/pallets/flask-sqlalchemy/issues/672#issuecomment-478195961">this suggestion</a> I deleted all legacy <code>.pyc</code> files inherited from the repository. The error persisted. I tried <a href="https://stackoverflow.com/a/56100377">this solution</a> and it did an amazing job paving the path clear for me. <strong>Until I found out that I was not able to create new user anymore.</strong> In other words, the local database was corrupted, and I could not upgrade/downgrade it anymore.</p>
<h2 id="correctwaystillaroundfileimports">Correct way: still around file imports.</h2>
<p>I almost gave up after googling and trying different things for about an hour. Intensive coding is fun, but I would like to get the ball rolling ASAP, or it&apos;s better to start from scratch.</p>
<p>I prefer to edit codes directly from text editors such as Atom/Notepad++. But I do have IDEs such as PyCharm, which I love to work with when navigating projects with complex code structure. Somehow I found that PyCharm highlighted the row <code>from config import Config</code> with red squiggly lines. I removed the whole <code>sys.path.append(file_dir)...</code> block from <code>__init__.py</code> and tried one last thing: <code>from app.config import Config</code>. And... the red squiggly lines went away? Inspired by this finding, I immediately fixed all imports within the <code>app</code> folder by adding &quot;app&quot; in front of the filename. The problem is finally solved!</p>
<h2 id="afterthoughts">Afterthoughts</h2>
<p>I wonder if it is because the <code>app</code> folder contains <code>__init__.py</code> thus Python 3.7 considered it as a package, therefore the &quot;app&quot; prefix is required to import the remaining files within the same folder. I vaguely recall myself reading a <a href="http://python-notes.curiousefficiency.org/en/latest/python_concepts/import_traps.html">note</a> on how frustrating it could be when dealing with Python import system. I probably should read it again.</p>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>A legacy Flask app could not run when migrating to another machine. It first complained that modules within the same folder cannot be found. Using some Googled trick I messed up the SQLite local database.</li>
<li>It&apos;s an issue with the Python import system.</li>
<li>I hate database migrations. (One main issue the team always ran into when I was a back-end intern for a web app)</li>
<li>Googled solutions do not always work; try at your own risk, but they may inspire you to find the right way.</li>
</ul>
<p>And I hope I could bring some good news next time! Looks like only after dealing with a tricky tech problem do I have the impulse to update here...</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[My (Not-so-Tough?) First-try with Docker]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h1 id="foreword">Foreword</h1>
<p>Learning Docker has been on my to-do list for a while. When I say &quot;a while&quot;, I mean it. (Circa 2017, or even earlier?)</p>
<p>I won&apos;t list the advantages of containerization here (as Google can tell you more), but I&apos;m a practical learner.</p>]]></description><link>https://blog.ruosilin.com/my-not-so-tough-first-try-with-docker/</link><guid isPermaLink="false">5eaf0a9f04d3b509de1802da</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Mon, 04 May 2020 01:15:04 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h1 id="foreword">Foreword</h1>
<p>Learning Docker has been on my to-do list for a while. When I say &quot;a while&quot;, I mean it. (Circa 2017, or even earlier?)</p>
<p>I won&apos;t list the advantages of containerization here (as Google can tell you more), but I&apos;m a practical learner. Which means I learn things by actually doing it. Back in 2017, I started this <a href="https://roselin.me/project/telegram-cat-bot/">Telgram catbot</a> project to share the love of my two cats with the world (side goals: improve my Python coding skills/learn Git). It was originally deployed on Heroku; given that I was on the free &quot;development&quot; tier and the popular demand (mostly from my friends), my &quot;dyno hours&quot; (aka available resources) quickly exhausted every month. I had to shut down the project last year so that my other projects could stream normally on Heroku. When I did the portfolio refresh this year, I realized that VPS is an ideal place to host my projects; it gives me more freedom than Heroku does (plus, I&apos;ve paid for it every month already). So which one should I migrate first? Catbot, my first and foremost (serious) project, becomes the top choice.</p>
<h1 id="dockerizing">Dockerizing...</h1>
<p>I won&apos;t spend too much time describing the efforts it took on reactivating the catbot project itself, as it&apos;s not too relevant. I do want to mention, though, that the bot was written in mid-2017 with Python 2.7. (I don&apos;t know what I was thinking back then, either - I didn&apos;t fully embrace Python 3 until early 2018 though) Also, the <a href="https://github.com/python-telegram-bot/python-telegram-bot"><code>Python-telegram-bot</code></a> framework recently received a major update on callback (now context-aware). Heroku only supports PostgreSQL, but with Ghost my server already has MySQL nicely set up. Moreover, Heroku recycles my applications once per day, but that won&apos;t be the case for my server, so I need to have the job set up correctly for the daily push to users. Took me a day to fix all these nuances, and I had the bot up and running in the local mode!</p>
<p>I&apos;m now ready to embrace Docker. However, I have no experience other than going through the official Docker starter doc once. That did not impress me, as I vaguely remember terms as &quot;images&quot;, &quot;hub&quot;, etc. What should I do?</p>
<blockquote>
<p>I&apos;d like to give credits to people on Twitter, though: below is a list of useful resources provided by them:<br>
<a href="https://pythonspeed.com/docker/">Production-ready Docker packaging</a><br>
<a href="https://yeasy.gitbook.io/docker_practice/">Docker - &#x4ECE;&#x5165;&#x95E8;&#x5230;&#x5B9E;&#x8DF5;</a> (Note: zh-cn only)<br>
Also Google! :)</p>
</blockquote>
<p>Somehow the two terms, &quot;yaml&quot; and &quot;Dockerfile&quot;, came to my mind first. What do they mean? Do I need both to run my bot? In a nutshell, the bot depends on a Python script, which communicates with a local database that persists user information. (Since version 12, Python-telegram-bot natively supports persistence. The only example is about persisting conversational handlers, which is irrelevant to my case. I browsed some other posts online, and they all suggested sticking with databases for persistence. So I did not break the tie with databases) Besides, the bot talks to <a href="https://cloudinary.com">Cloudinary</a> and needs a Telegram bot token passed from the environment. Thus, I need to take all API secrets into consideration as well.</p>
<p>I chose to start with &quot;yaml&quot;, which is Docker composer. :)</p>
<h2 id="challenge1cannotrundockercomposeup">Challenge 1: cannot run <code>docker-compose up</code></h2>
<pre><code>ERROR: Version in &quot;./docker-compose.yml&quot; is unsupported. You might be seeing this error because you&apos;re using the wrong Compose file version. Either specify a version of &quot;2&quot; (or &quot;2.0&quot;) and place your service definitions under the `services` key, or omit the `version` key and place your service definitions at the root of the file to use version 1.
For more on the Compose file format versions, see https://docs.docker.com/compose/compose-file/

</code></pre>
<p>I did not have <code>docker-composed</code> install on my sandbox originally, so I followed the command prompt as it suggested me to grab the app through <code>apt-get</code>. Then I tried <code>docker-compose up</code> again, and it returned the error as shown above. I could change the version from 3 to 2 and make it work, but I wonder why. I have Docker version == 19.03.8, which is supposed to support version 3. <a href="https://github.com/bigbluebutton/greenlight/issues/228#issuecomment-545919537">This comment</a> helped me out, seems that I didn&apos;t get <code>docker-compose</code> installed correctly through <code>apt-get</code>.</p>
<h2 id="challenge2mycontainerexitsitself">Challenge 2: ... my container exits itself?</h2>
<p>Yes, it always quitted with exit code 0. I&apos;m following the example <a href="https://www.techrepublic.com/article/how-to-build-a-docker-compose-file/">here</a>, but my yaml file looks like below:</p>
<pre><code>version: &quot;3.0&quot;

services:
  catbot-container:
    image: python:3.6-slim-buster
    env_file: env.list
</code></pre>
<p>... it just exited. Not until now did I realize that I started an empty container with nothing in it. Thus it gracefully exited. Since my app is a simple one and does not require multi-container interactions, I decided to give Dockerfile a try.</p>
<h2 id="challenge3ireallydontknowhowdockerworks">Challenge 3: I really don&apos;t know how Docker works.</h2>
<p>I adopted the &quot;somewhat better image&quot; example in <a href="https://pythonspeed.com/articles/dockerizing-python-is-hard/">this post</a> as a starting point and made some modifications. Of course, I did not get it right the first try. After trials and errors, I believe the groundwork is finally done. What&apos;s next?</p>
<p>Well, I then managed to run <code>docker build</code>. Looks like this command somehow turns my configuration into an image that is replicable across different machines.</p>
<p>The build succeeded. What does that mean? After couple hours of googling, I found that I should run <code>docker run</code>. There are some flags that I have to be mindful about.</p>
<ul>
<li>I&apos;d like the container appears to be running on the host itself (from the perspective of the network) so it could seamlessly connect to the localhost database, so I added <code>--net=host</code></li>
<li>There are too many secrets to pass in single run command, therefore it&apos;s loaded from a file called <code>env.list</code>: <code>--env-file=env.list</code></li>
<li>My app will not quit itself easily. I would prefer it running silently in the background while I&apos;m working on some other stuff. Thus, <code>--detach</code>.</li>
<li>Even after I reboot my server, the container should come back to life ASAP. I updated the restart status afterward, but could have done so in the run command by adding <code>--restart=unless-stopped</code></li>
</ul>
<p>This seems to be a long run command to me, but I&apos;m pretty sure there are longer ones out there (maybe why people eventually choose to use composer to handle all these?). After running <code>docker run --detach --env-file=env.list --name NAME --net=host BUILD</code>, I checked the status of the container and boomed! Everything is up and running.</p>
<h1 id="now">Now</h1>
<p>Sometimes, a picture worths more than a thousand words:</p>
<p><img src="https://lh3.googleusercontent.com/proxy/WlJRKxefnqM6OmkWj0IujkDSzvLDwO2wssTW_SvSbrhgJaGWhZVlzeJcfh7ygdQ8JW5kE3H2iUYMIXP1WSov3vb3R0T7osEBP8iur6jxfM0XAzo70DX4PDCtKO8V0CTAmMrd6IAvtmsv2yNsO8eWqV51DX0vg3C_UZE" alt="image" loading="lazy"></p>
<p>I tried restarting my server a couple of times, and the cat bot is still up and running :O. You are more than welcomed to test it out <a href="https://t.me/daliycatie_bot">here</a> (A valid Telegram account required). Based on the logic, the daily push is supposed to be functioning. We will see how it goes tomorrow.</p>
<p>The original title is &quot;My (Tough) First-try with Docker&quot;. I was still unclear about docker compose and Dockerfile back then. I feel much better now though after actually building an image and starting a container myself. It only took me half day (less than 4 hours)! I can see it comes in handy for reproducible machine learning projects (passing my beautifully boxed DL codes to other DS) without the hassle of manual setup. Also, I love the restart option - no more tmux session gone after system restart!!</p>
<p>Hope you enjoy this little post (with grumbles) and stay safe during this uncertain time. It is said that staying at home is an ideal time to pick up new skills; but still, physical health comes first. Working on resurrecting legacy projects does bring joys to me during this remorseful time, though.</p>
<!--kg-card-end: markdown--><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Why My Saved Model Is Not Working as Expected?]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Update 2/6: After reporting our findings back to the client, we found out that the dataset we used was biased. Not the typical &quot;target = 1&quot; cases, but the training data did not reflect the reality well. Therefore the model suffers, too. &quot;Garbage in, garbage out.&quot;</p>]]></description><link>https://blog.ruosilin.com/why-my-saved-model-is-not-working-as-expected/</link><guid isPermaLink="false">5e1d0a9ef870380a0ce90e01</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Tue, 14 Jan 2020 02:06:06 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>Update 2/6: After reporting our findings back to the client, we found out that the dataset we used was biased. Not the typical &quot;target = 1&quot; cases, but the training data did not reflect the reality well. Therefore the model suffers, too. &quot;Garbage in, garbage out.&quot; The team has learned the lesson and the next dataset would be holistic and representative.</p>
<p>Well - the title pretty much summarizes what I have been doing in the past week.</p>
<p>Long story short:</p>
<ul>
<li>there was a business problem to solve. (Due to intellectual property rules, cannot say more beyond this. Think of it as a binary classification problem.)</li>
<li>We (Data Scientists) came up with a solution (a model for sure).</li>
<li>Before deploying the model into production, we tested it out for a certain period and collected the results.</li>
<li>Due to the time lags in some additional processing, the results were not available until last week.</li>
<li>We were then shocked by the news: for the first three groups that were supposed to have a precision of 40%, we only got 9%.</li>
</ul>
<h2 id="wtf">WTF??</h2>
<p>Seriously, that was my first thought. Moreover, we (data scientists) did not see any metrics other than the summarized statistics (Basically a statement on a slide: for the top 3 groups, the precision is 9%. Model precision: 40%). Though the training set we used was not the most recent, we certainly did not expect to see such a dramatic difference. (Around 5% fluctuation is understandable) But we have told the engineers that they should start implementing this model in two weeks! We had to miss the deadline once due to the developers&apos; limited capacity. If we wait again, it would be another three months. Plus, once our managers know they would need a  model diagnostic writeup.</p>
<h2 id="modeldiagnostics">Model Diagnostics</h2>
<p>To save our butts, I reached out to analysts who collected the testing results and calculated the performance figure. She sent me back a spreadsheet with unique identifiers (objects that we are predicting) and some attributes. Without firing up any weapons from my toolkit, I realized the first problem: <strong>attributes reported in the validation report do not match with the target requirement</strong>.</p>
<p>For instance, let&apos;s say that we would like to build a model to predict whether a given day would be ideal for a day trip. For a day to be qualified as &quot;ideal&quot;, it must meet the following two criteria:</p>
<ul>
<li>The expected $ I spent would be less than $500</li>
<li>The weather between 10 am to 12 pm is not rainy</li>
</ul>
<p>But the report only contains information such as &quot;weather between 8 am to 11 am&quot;, and &quot;the expected $ the team spent&quot;. These two pieces of information are then put together and treated as the target. You can&apos;t say there is no value in recording the two attributes; in fact, there is an overlap. However, this is not what we agreed upon. When gauging the model performance, we need to refer back to the model definition and capture exactly the needed features. Otherwise, it&apos;s not an apples-to-apples comparison.</p>
<p>Another problem lies in the model attribute distributions. I compared how the distributions of selected features differ from the training set, testing set, and (essentially) the holdout testing set. Very few features have similar distributions. A machine learning model learns what was presented to it during the training time. This is not even a generalization problem - over time, some features may have drifted and that is a call for more recent data. Consider Twitter spam behaviors. If the model was trained using 2014 data, of course, the model will fail in finding 2020 spammers. There is no guarantee that over time, features in our model have constant distributions.</p>
<p>The third problem is the most interesting one, and it deserves a section itself.</p>
<h2 id="becarefulwithpipelines">Be careful with Pipelines...</h2>
<p>When I trained a model on the server and immediately used it to predict the training dataset (Caution: THIS PRACTICE IS NOT ENCOURAGED), each instance got an assigned probability. I saved the model to my local workstation, load the same training dataset, and used the saved model to generate predictions. Probabilities changed for the same instance! Why? I have the seed set, and the model configuration (i.e. parameters of the model) is what I need. The way I calculated model performance metrics (accuracy, precisions, etc) is correct. The model pickle file was not contaminated with random noises. Could it be because of the different environment setups? I was under the impression that a trained ML model should always spit out the same output given the same input! After spending a whole morning debugging (in Jupyter Notebook), I was pulling my hair out, crying.</p>
<p>At first glance, this seems to be a problem with recent <code>scikit-learn</code> updates. My local was running 0.22.1 while the server had 0.21.3. It should be a major update, because my local machine refused to load the saved pickle file at all. For compatibility, I had to downgrade <code>scikit-learn</code> on my end. Now the local environment loaded the saved pickle file correctly, but the predictions were still everywhere!</p>
<p>Side note: instead of the model (e.g. SVM, GBM, logistics regression etc.), I saved the whole sk-learn <code>Pipeline</code> object. The pipeline contains some preprocessing steps, such as handling missing values. But these steps are bare minimal. Before training the model, I had to take an extra step to clean the training data, such as removing dots and dollar signs, binning into different groups, keeping top <code>n</code> values, etc. These preprocessing are considered as &quot;data transformations&quot; and thus not included in the model pipeline.</p>
<p>... You probably have an idea of what went wrong now after reading my notes above. And that&apos;s exactly the problem - when I unpickled the saved pipeline object locally, I fed it the raw training data directly. <strong>The data transformation step was omitted completely at the inference time</strong>. During the training phase, the model saw a feature split nicely into 10 bins; but at the inference time, even the input data is the same, because of the missing data transformation step, now the model had to deal with the raw data instead of the transformed 10 bins. After adding all required data transformations, the saved model pickle returns the same result for a given instance.</p>
<p>So what could I do?</p>
<ol>
<li>Fix the Pipeline and pickle again, so that the saved pickle contains the required data transformation. In production, the model handles raw data naturally.</li>
<li>Have the engineers transformed the data first before sending it to our model.</li>
</ol>
<p>2 is the preferred way by our developers, as they have some clever procedures to tweak data transformations in real-time. So all we need to do now is to apply the transformation on the holdout, beta test data and collect model outputs. Since the field test already concludes, with the correct target information we can measure how well the model is performing.</p>
<h2 id="insummary">In summary</h2>
<p>Don&apos;t be afraid if your model performs poorly on unseen data. Take the steps to debug it (I would have to say: different from debugging codes. I miss VS debuggers!):</p>
<ul>
<li>If you are not the one who collects test results, ask the relevant party to provide information. Specifically, what attributes were captured, how were they calculated the performance metrics.</li>
<li>Compare against your training data to see if attribute distributions change over time (think of Twitter spamming behaviors - spammers change frequently to evade rules).</li>
<li>If data pipeline/preprocessing is involved, check if raw data was exposed to the model directly, with any critical preprocessing steps missing.</li>
<li>Keep asking questions! (Even under tight deadlines)</li>
</ul>
<p>I should have taken a closer look at the data transformation step earlier, but naively I thought it was handled by the sklearn Pipelines already. It&apos;s not until I dissected the pickle file that I realized how data transformation came into play. Also: <em>don&apos;t feed your trained model with training data again</em>. It&apos;s not a good way to judge model performance... overfitting is inevitable unless you know what you&apos;re doing (in this case, I do). Happy coding! Hopefully, I could update more frequently in 2020; I do feel that mistakes are my main drive force for writing blogs nowadays&#x1F602;&#x1F602;&#x1F602;</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Feature Engineering for Machine Learning - Chapter 2 note]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h1 id="featureengineeringformachinelearning">Feature Engineering for Machine Learning</h1>
<h2 id="ch2fancytrickswithsimplenumbers">Ch.2: Fancy Tricks with Simple Numbers</h2>
<ul>
<li>Most ML algorithms can only take numerical inputs. Feature engineering on numeric features: basic &amp; critical.</li>
<li>First sanity check: magnitude. Would a coarse granularity work instead of the actual value?</li>
<li>Tricks to deal with counts, which may grow</li></ul>]]></description><link>https://blog.ruosilin.com/feature-engineering-for-machine-learning-chapter-2-note/</link><guid isPermaLink="false">5d64912aecf5420866118e41</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Tue, 27 Aug 2019 02:11:29 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h1 id="featureengineeringformachinelearning">Feature Engineering for Machine Learning</h1>
<h2 id="ch2fancytrickswithsimplenumbers">Ch.2: Fancy Tricks with Simple Numbers</h2>
<ul>
<li>Most ML algorithms can only take numerical inputs. Feature engineering on numeric features: basic &amp; critical.</li>
<li>First sanity check: magnitude. Would a coarse granularity work instead of the actual value?</li>
<li>Tricks to deal with counts, which may grow without limitation:
<ul>
<li>Binarization: turns the problem into 0/1, occurred/not occurred.</li>
<li>Binning: useful when the distribution is heavy-tailed
<ul>
<li>Fixed-width binning: Easy to compute, but may end up with multiple empty bins. 1) heuristics (e.g. group by age), 2) log for exponential-width binning, dividing by a constant for linear-width binning.</li>
<li>Quantile binning: adaptively positions the bins based on the distribution of the data. Quantile, decitile, etc.</li>
</ul>
</li>
</ul>
</li>
<li>Log transformation: effective for heavy-tailed distribution. &quot;It compresses the long tail in the high end of the distribution into a shorter tail, and expands the low end into a longer head.&quot;
<ul>
<li>It&apos;s not a one-time, save-all technique though. Need to inspect the relationship between the transformed feature and the target to see if model assumption holds.</li>
</ul>
</li>
<li>Power transformation: extension of log transformation
<ul>
<li>AKA &quot;variance-stabilizing transformations&quot;: e.g. on Poisson, power transformation removes the dependency between variance &amp; mean.</li>
<li>Box-cox: a simple generalization of both the square root transform and the log transform. Only works for positive data.</li>
</ul>
</li>
<li>Feature scaling: <strong>doesn&#x2019;t change the shape of the distribution</strong>; only the scale of the data changes. Useful where a set of input features differ wildly in scale. For certain model, drastically varying scale in input features may lead to numeric instability.
<ul>
<li>Min-Max Scaling: squeezes (or stretches) all feature values to be within the range of [0, 1]</li>
<li>Standardization (Variance Scaling): scaled feature would have mean = 0 and variance = 1</li>
<li>L2 normalization: outputs will have norm 1. Not necessarily in the feature space; could be in the data space as well.</li>
</ul>
</li>
<li>Don&apos;t scale sparse data -&gt; dense vector; huge computation burden on the classifier!</li>
<li>Complex features, e.g. interactions (products of two features)
<ul>
<li>Do NOT need to curate manually for decision tree-based models; could help generalized linear models (to leverage logical AND relationship)</li>
<li>Easy to compute, but increases computation complexity (now has to consider second order features as well)</li>
<li>To account for the computational expense of higher-order interaction features: feature selection &amp; handcrafted small sets. Both have their advantages &amp; drawbacks.</li>
</ul>
</li>
<li>Feature selection: prunes away non-useful features in order to reduce the complexity of the resulting model.
<ul>
<li>Filtering: preprocess features to remove ones that are unlikely to be useful for the model. Computationally cheaper than the wrapper methods, but lack consideration for the underlying model. (May not select the right features for the model)</li>
<li>Wrapper methods: essentially using a subset of features and gradually growing. Expensive, but not doing pruning upfront. &quot;The wrapper method treats the model as a black box that provides a quality score of a proposed subset for features. There is a separate method that iteratively refines the subset.&quot;</li>
<li>Embedded methods: feature selection is included as part of the model training process. Less powerful than wrapper, but not computationally expensive; better than filtering as it relies on the underlying model. A balance between computational expense &amp; quality of results.</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Make a fair coin from a biased one, and more...]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I read about this question online today and found it interesting [<a href="https://www.geeksforgeeks.org/print-0-and-1-with-50-probability/">source</a>]:</p>
<blockquote>
<p>You are given a function foo() that represents a biased coin. When foo() is called, it returns 0 with 60% probability, and 1 with 40% probability. Write a new function that returns 0 and 1 with 50% probability</p></blockquote>]]></description><link>https://blog.ruosilin.com/make-a-fair-coin-from-a-biased-one-and-more/</link><guid isPermaLink="false">5d586fb6ecf5420866118dbc</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Sat, 17 Aug 2019 22:06:43 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>I read about this question online today and found it interesting [<a href="https://www.geeksforgeeks.org/print-0-and-1-with-50-probability/">source</a>]:</p>
<blockquote>
<p>You are given a function foo() that represents a biased coin. When foo() is called, it returns 0 with 60% probability, and 1 with 40% probability. Write a new function that returns 0 and 1 with 50% probability each. Your function should use only foo(), no other library method.</p>
</blockquote>
<p>You can easily find the solution in the link above, but I thought it in the wrong way: my first attempt was to return 0 if two tosses return (1,0) and (0,1), 1 otherwise. Apparently the math does not add up here: <code>P(1,0) + P(0,1) = 0.52</code>. Later I learned that John von Neumann has proposed an elegant <a href="https://en.wikipedia.org/wiki/Fair_coin#Fair_results_from_a_biased_coin">algorithm</a> to solve it (though not 100% efficient). Naturally, I asked myself: can I use the same unfair coin to generate 1, 2 and 3 with equal probability (i.e. each with chance 1/3)?</p>
<p>... I took the wrong path again. Trying to group all combinations by tossing the coin 3 times into 3 groups: (000, 111) -&gt; discard &amp; continue to toss; {1 one, 2 zeros} and {2 ones, 1 zero}. Unfortunately <code>P(1 one, 2 zeros) != P(2 ones, 1 zero)</code> plus I can&apos;t get 3 numbers out of this grouping mechanism. Someone on Twitter proposed the following:</p>
<blockquote>
<p>Generate 3 bits. Return 0 if 100/011, 1 if 010/101, 2 if 001/110, start over if 000/111.</p>
</blockquote>
<p>Inspired by the 3 case, I think the following should work if I were to use the same coin to generate 1-5 with equal probabilities:</p>
<blockquote>
<p>Generate 5 bits. There will be 2^5 = 32 combinations in total. Occurrences of {1 one, 4 zeros} = {4 ones, 1 zero} = 5, whereas {2 ones, 3 zeros} = {3 ones, 2 zeros} = 10. Return each number by exhausting the following sequence: (1 combination from {1 one, 4 zeros} , 1 combination from {4 ones, 1 zero}, 2 combinations from {2 ones, 3 zeros}, 2 combinations from {3 ones, 2 zeros}). Repeat the process if either {00000, 11111} is obtained.</p>
</blockquote>
<p>Note: the above process could be generalized to other prime, odd numbers. Non-prime odd numbers fail as there exists an odd number <code>d</code>, <code>2k &lt; d</code> such that <code>d</code> cannot divide <code>C(d, 2k)</code> without a remainder. E.g. <code>d=9, k=3</code>.</p>
<p>It&apos;s entertaining to think through this kind of probability questions sometimes. Happy weekend!</p>
<!--kg-card-end: markdown--><p></p>]]></content:encoded></item><item><title><![CDATA[Solutions for Python Challenge]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>You may prefer tackling the challenge by yourself first before referring to my solutions &#x1F600;. Off you go: <a href="http://www.pythonchallenge.com/">Python Challenge</a></p>
<h1 id="challenge0">Challenge 0</h1>
<p>Just get <code>2^38</code> and substitute the result into the URL.</p>
<h1 id="challenge1">Challenge 1</h1>
<p>Code:</p>
<pre><code>s = &quot;g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc</code></pre>]]></description><link>https://blog.ruosilin.com/solutions-for-python-challenge/</link><guid isPermaLink="false">5d0c30ceecf5420866118d70</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Fri, 21 Jun 2019 03:54:28 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>You may prefer tackling the challenge by yourself first before referring to my solutions &#x1F600;. Off you go: <a href="http://www.pythonchallenge.com/">Python Challenge</a></p>
<h1 id="challenge0">Challenge 0</h1>
<p>Just get <code>2^38</code> and substitute the result into the URL.</p>
<h1 id="challenge1">Challenge 1</h1>
<p>Code:</p>
<pre><code>s = &quot;g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr&apos;q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj.&quot;
#s = &quot;map&quot;
translated = &quot;&quot;
for c in s:
   if c.isalpha() and c &lt; &apos;x&apos;:
     translated += chr(ord(c)+2)
   elif c.isalpha():
     translated += chr(ord(c)-24)
   else:
     translated += c
print(translated)
</code></pre>
<p>It&apos;s a simple mapping - every letter in the encrypted text should be moved by 2 (hence we get &apos;m&apos; for &apos;k&apos;, &apos;o&apos; for &apos;m&apos; etc). Notice the special case: when the letter is <code>y</code> and <code>z</code>, you can&apos;t just refer to the ascii table as it will return you non-letter. Basically &apos;y&apos; is for &apos;a&apos; and &apos;z&apos; is for &apos;b&apos;, so a special handling is required. Once you decode the helper text, the next part is just applying it to the URL.</p>
<h1 id="challenge2">Challenge 2</h1>
<p>Code:</p>
<pre><code>import string
s = &quot;&quot;&quot; 
(the text you get from the source)
&quot;&quot;&quot;
translated = &quot;&quot;

for c in s:
  if c not in string.punctuation and c != &apos;\n&apos;:
    translated += c
print(translated)
</code></pre>
<p>I have to admit that this is not the MOST elegant solution (maybe regex could handle it better, but I&apos;m not a fan of regex afterall). When inspecting the page source you should be able to find a big chunck of text that&apos;s mostly composed of punctuation marks. You can then parse it using <code>string.punctuation</code>.</p>
<h1 id="challenge3">Challenge 3</h1>
<p>Code: (not using regex here)</p>
<pre><code>s = &quot;&quot;&quot;(the big string in the source code)&quot;&quot;&quot;
s = s.replace(&apos;\n&apos;, &apos;&apos;).replace(&apos;\r&apos;, &apos;&apos;)

res = &quot;&quot;
i = 0
for i in range(len(s)-8):
  current = s[i:i+9]
  if current[0].islower() and current[1:4].isupper() and current[5:8].isupper() and current[-1].islower() and current[4].islower():
    res += current[4]
print(res)
</code></pre>
<p>I know, I know very well that the solution here is ugly. BUT IT WORKS! Basically you should be looking for patterns like <code>xXXXaXXXx</code>, where <code>a</code> is the desirable letter. It&apos;s a bit elusive to read the hint this time, but thanks to <a href="https://groups.google.com/forum/#!topic/python-challenge/QK82750x3GU">this post</a> I finally got it to work (not in a nice way).</p>
<h1 id="challenge4">Challenge 4</h1>
<pre><code>import urllib3

http = urllib3.PoolManager()

path = &apos;http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing=&apos;
num = &quot;12345&quot;
res = [num]

for i in range(400):
    r = http.request(&apos;GET&apos;, path+num)
    if r.status == 200:
        num = r.data.split()[-1]
        res.append(num)

print(res)
</code></pre>
<p>The hint is buried in the back again: don&apos;t try to loop it for over 400 times. It will manifest itself at index 357 (am I disclosing too much?)</p>
<h1 id="challenge5">Challenge 5</h1>
<pre><code>import pickle
from urllib2 import urlopen

src = &quot;http://www.pythonchallenge.com/pc/def/banner.p&quot;

test = pickle.load(urlopen(src))
for line in test:
    print(&quot;&quot;.join([k * v for k, v in line]))
</code></pre>
<p>This is clever hahahaha. I didn&apos;t think of the solution myself (thanks for the <a href="https://www.hackingnote.com/en/python-challenge-solutions/level-5">reference</a>!), but with an extension like <code>.p</code> I should have realized that it&apos;s related to pickle. Once you unpickle it, the way to put it back also amuses me (list of tuples, not fun to deal with huh).</p>
<h1 id="challenge6">Challenge 6</h1>
<!--kg-card-end: markdown--><p></p>]]></content:encoded></item><item><title><![CDATA[Cloudflare integration, and more...]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>This is my very first post in China (and hopefully not the last one lol). So far so good, except that my server IP was recently blocked due to service misuse (<a href="https://shadowsocks.org/">more info</a>). Have learned several things along the way, but first I need to rescue my blog &amp; portfolio</p>]]></description><link>https://blog.ruosilin.com/cloudflare-integration-and-more/</link><guid isPermaLink="false">5cfa6e2cecf5420866118c6a</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Fri, 07 Jun 2019 14:52:05 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>This is my very first post in China (and hopefully not the last one lol). So far so good, except that my server IP was recently blocked due to service misuse (<a href="https://shadowsocks.org/">more info</a>). Have learned several things along the way, but first I need to rescue my blog &amp; portfolio site so that they are accessible in China. As a side note, the domain was purchased from <a href="https://www.namecheap.com/">Namecheap</a> and DNS was configured under <a href="https://www.digitalocean.com/">DigitalOcean</a>.</p>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Use Cloudflare to hide your server IP, protect your sites from potential hacks, and fast loading in different parts of the world.</li>
<li>Don&apos;t force a single VPS to wear multiple hats (like web hosting, mail server, and SS).</li>
<li>If your VPS image handles both web hosting and mail service together, manage carefully on Cloudflare to ensure that mail server works just fine.</li>
</ul>
<h2 id="whycloudflare">Why Cloudflare?</h2>
<p>This is <a href="https://www.cloudflare.com/learning/what-is-cloudflare/">what they said</a>...</p>
<blockquote>
<p>Cloudflare also provides security by protecting Internet properties from malicious activity like DDoS attacks, malicious bots, and other nefarious intrusions.</p>
</blockquote>
<p>Basically it is a free <strong>content delivery network (CDN)</strong> that speed up page streaming based on geolocations. In my previous DNS setup, the blog subdomain directly pointed to the server IP - a simple <code>ping</code> would uncover the IP immediately. It was not until the IP was blocked in China did I realize that this is a dangerous act. With Cloudflare, the domain name will not expose the server IP anymore. Since the domain is now handled by Cloudflare, my websites should be viewable in China (and even faster!) as long as their associated IPs are not blocked. <em>It is to my understanding that the associated IP would change periodically (or location based?), not confirmed though.</em></p>
<h2 id="howtoconfigurecloudflare">How to configure Cloudflare?</h2>
<p>Simple -</p>
<ol>
<li>Register an account on Cloudflare</li>
<li>Enter domain names. Cloudflare will automatically scan existing DNS records for you.</li>
<li>Confirm that the captured DNS records match with your current setting.</li>
<li>Update your nameserver information at your domain provider.</li>
<li>Wait a bit (for DNS praprogation), and you&apos;re good to go!</li>
</ol>
<p>It really takes less than 5 minutes. <a href="https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website">This guide</a> provides more details.</p>
<h2 id="ughhardlessons">Ugh, hard lessons</h2>
<p>This is my first time setting up a VPS, so I was greedy: I used it for web hosting, mail forwarding, and shadowsocks (SS) server. <strong>THIS IS NOT A GOOD PRACTICE</strong>. Specifically for Digital Ocean, it is encouraged to separate these tasks into different droplets (so that if one is down, others will stay intact and function normally). But for me, since all these features were under the same droplet, their performances are dependent upon each other. As the IP was blocked in China (due to SS), requests sent to my hosted websites timed out indefintely. (It is said that the IP will be removed from blacklists sometimes in the future, but as of today no) My turnaround was to use CDN (i.e. Cloudflare), but then all DNS records are maintained by Cloudflare instead. The provider did a great job auto-scanning and importing existing DNS rows from Digital Ocean - <strong>except for the mail server.</strong></p>
<p>The moment I turned Cloudflare on, I stopped receiving any emails from my domain mailbox. I didn&apos;t realize this until two days later, when Gmail prompted that my test email was undeliverable. The error message is displayed below:</p>
<p><img src="https://blog.ruosilin.com/content/images/2019/06/mailfailed.JPG" alt="mailfailed" loading="lazy"></p>
<p>I then found that the 4 IPs belong to Cloudflare. Below was a screenshot of my previous DNS setting:</p>
<p><img src="https://user-images.githubusercontent.com/16634756/59111962-b1aa1180-8907-11e9-8835-311b8ac25746.png" alt="previous-DNS" loading="lazy"></p>
<p>Notice the two rows highlighted in yellow. These records determine how mail DNS works and I got both wrong.</p>
<p>When the <code>A</code> record for mail got a grey cloud, Cloudflare warns that the record was exposing my IP. This is the right way to go, though; Cloudflare does not host my mail server, so no HTTP proxy is required (only DNS). Moreover, <code>name</code> of the <code>MX</code> record should be the domain name. I spent two hours trying to figure out why Postfix suddenly stopped forwarding emails to my personal mailbox, only to find that it&apos;s not Postfix&apos;s fault but DNS issues :P. Now I have it setup this way and the mail server is working as usual:</p>
<p><img src="https://user-images.githubusercontent.com/16634756/59112693-23368f80-8909-11e9-986f-9be52a7165b8.png" alt="now-DNS" loading="lazy"></p>
<h2 id="inshort">In short...</h2>
<p>Don&apos;t be aggressive and put all eggs into the same basket. If you do and it fails, take the failure as a way to learn and remedy right away lol.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[*Graduated!*]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>The day has FINALLY come. Bye school! It&apos;s like a dream. Haven&apos;t seen my parents for 20 months and they flied over just to show up &amp; support. Super grateful &#x1F497;</p>
<p>Missions accomplished during the past 2 years:</p>
<ul>
<li>Graduated with 4.0 GPA! (10 classes in</li></ul>]]></description><link>https://blog.ruosilin.com/graduated/</link><guid isPermaLink="false">5cd8e0b203b8990840fc5446</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Mon, 13 May 2019 03:31:26 GMT</pubDate><media:content url="https://blog.ruosilin.com/content/images/2019/05/IMG_20190319_190511.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://blog.ruosilin.com/content/images/2019/05/IMG_20190319_190511.jpg" alt="*Graduated!*"><p>The day has FINALLY come. Bye school! It&apos;s like a dream. Haven&apos;t seen my parents for 20 months and they flied over just to show up &amp; support. Super grateful &#x1F497;</p>
<p>Missions accomplished during the past 2 years:</p>
<ul>
<li>Graduated with 4.0 GPA! (10 classes in total)</li>
<li>Completed 2 internships and received return offers from both (and felt so sorry to turn one down. They were both AMAZING!)
<ul>
<li>One of the internship is related to <em>data science</em> - the primary reason why I went back to school! Almost gave up hope on that (thought that I&apos;d have to obtain a PhD first before even trying to submit an application LOL), maybe could write a post sharing my experience in the near future ...?</li>
</ul>
</li>
<li>Got to work with talented people. My professors are not only knowledgable but also kind. I&apos;ve learned a ton from them outside of the academic subjects~</li>
<li>Made new friends. Hopefully our relationships will be long lasting!</li>
<li>It&apos;s just great to be an Aggie.</li>
</ul>
<p>Bye, College Station. I will carry the memory along and step forward. Next step: Dallas~</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.ruosilin.com/content/images/2019/05/IMG_20170825_151156.jpg" class="kg-image" alt="*Graduated!*" loading="lazy"><figcaption>My very first time on Military Walk, Aug 2017</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[How to: migrate from Jekyll]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Today marks my 2nd day with Ghost &#x1F618; I spent nearly a whole night yesterday trying to figure out how to migrate from my current Jekyll site (hosted under Github pages). All my technical notes are stored there; I did think about moving my wordpress.com content over, but that&</p>]]></description><link>https://blog.ruosilin.com/how-to-migrate-from-jekyll/</link><guid isPermaLink="false">5cce33bb504b022a7c669760</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Sun, 05 May 2019 01:25:37 GMT</pubDate><media:content url="https://blog.ruosilin.com/content/images/2019/05/jtog.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://blog.ruosilin.com/content/images/2019/05/jtog.png" alt="How to: migrate from Jekyll"><p>Today marks my 2nd day with Ghost &#x1F618; I spent nearly a whole night yesterday trying to figure out how to migrate from my current Jekyll site (hosted under Github pages). All my technical notes are stored there; I did think about moving my wordpress.com content over, but that&apos;s another story (I used to write a post about <a href="https://mekomlusa.wordpress.com/2016/05/15/deal-with-wordpress-extended-rss-wxr/">how to effectively extract information from WXR</a>. In Chinese though). Nonetheless, it turned out to be a much challenging task.</p>
<p><img src="https://blog.ruosilin.com/content/images/2019/05/Capture05042019.JPG" alt="How to: migrate from Jekyll" loading="lazy"><br>
<em>My current Jekyll site. Simple yet powerful.</em></p>
<p>I have a good starting point: the <a href="https://github.com/mattvh/Jekyll-to-Ghost">unofficial plugin</a> recommended on the <a href="https://docs.ghost.org/api/migration/#non-official">migration guide</a>. However, the script is seriously outdated (last updated 5 years ago); it&apos;s not surprising to find that it failed in multiple places.</p>
<h2 id="challenge1runjekyllbuild">Challenge 1: Run <code>jekyll build</code></h2>
<p>This is for Windows 10 user only. As a ThinkPad user I really don&apos;t want to compromise and install Jekyll using some tricks. The <a href="https://jekyllrb.com/docs/installation/windows/#installation-via-bash-on-windows-10">official Jekyll docs</a> provide another way out: using bash.</p>
<h2 id="challenge2undefinedmethodgetconverterimpl">Challenge 2: <code>undefined method </code>getConverterImpl&apos;`</h2>
<p>This method seems to be deprecated after Jekyll 1.4.1 (<a href="https://www.rubydoc.info/gems/jekyll/1.4.1/Jekyll/Site:getConverterImpl">this</a>) is the last reference I&apos;ve found); the alternative is to manually update this line to be <code>find_converter_instance</code> (<a href="https://github.com/mattvh/Jekyll-to-Ghost/issues/7">source</a>).</p>
<h2 id="challenge3importerror">Challenge 3: Import error</h2>
<p>See the screenshot below. I also noticed people reporting the same issue on the <a href="https://forum.ghost.org/t/import-from-wordpress-to-ghost-2-0/3097">forum</a> (not Jekyll, but we have the same symptom).</p>
<p><img src="https://user-images.githubusercontent.com/16634756/57186652-e100d500-6ea8-11e9-9825-c0d51691b731.png" alt="How to: migrate from Jekyll" loading="lazy"></p>
<p>Looks like the workaround is to grab a Ghost 1.0 site on a Docker image. I don&apos;t know how to use Docker yet (willing to learn) but really don&apos;t want to go through the setup again (partially because my local machine is not Linux/Mac &#x1F602;). So... I spent some time diving into the <a href="https://docs.ghost.org/api/migration/#example">guide</a> again, and found what elements were missing.</p>
<ul>
<li>The fields required under <code>post</code> were changed. Especially the <code>&quot;mobiledoc&quot;</code> part.</li>
<li>Existing script does not include any author information, which <strong>might</strong> exist if the user has specified <code>authors.xml</code> under the <code>_data</code> folder (<a href="https://dev.to/m0nica/how-to-add-author-bio-to-posts-in-jekyll-3g1">source</a>).</li>
</ul>
<p>Initially, I would like to write another script using my favoriate language (Python, yes) to further process the generated JSON. But then I realized why not just modify the ruby file directly? And, FYI, last time I ever wrote any line in Ruby was back in Fall 2017. I can still read the code (somehow) but had no confidence in writing. Yet, I made a way to enhance the plugin so that it works for Ghost 2.0 import, and you can find the code and how to use it under my <a href="https://github.com/mekomlusa/Jekyll-to-Ghost">repo</a>.</p>
<h2 id="andfinally">And finally...</h2>
<p>All my 8 posts were migrated successfully! Just a side note: <strong>when manually adding the author information, make sure that the <code>slug</code> field has the same slug as you set in your current dashboard.</strong> That way, Ghost will automatically link the current users with all imported posts.</p>
<p><img src="https://user-images.githubusercontent.com/16634756/57186748-89fbff80-6eaa-11e9-9665-cc2378021f05.png" alt="How to: migrate from Jekyll" loading="lazy"><br>
<em>My site now with the two sample imported posts.</em></p>
<p>I still need to update the script so that it knows how to extract author info from Jekyll. If you have an idea how to do so, please let me know (have been trying different combinations with no luck)! Happy blogging &amp; coding &#x1F600;&#x1F600;&#x1F600;</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Hello World!]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Testing to see if <a href="https://ghost.org/">Ghost</a> is up and running.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.ruosilin.com/content/images/2019/05/cat20170808--34-.jpg" class="kg-image" alt loading="lazy"><figcaption>Little princess feeling dizzy when we were eating.</figcaption></figure>]]></description><link>https://blog.ruosilin.com/hello-world/</link><guid isPermaLink="false">5ccc6a692fe0bf39e9a3aa22</guid><dc:creator><![CDATA[Rose Lin]]></dc:creator><pubDate>Fri, 03 May 2019 16:21:19 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>Testing to see if <a href="https://ghost.org/">Ghost</a> is up and running.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.ruosilin.com/content/images/2019/05/cat20170808--34-.jpg" class="kg-image" alt loading="lazy"><figcaption>Little princess feeling dizzy when we were eating.</figcaption></figure>]]></content:encoded></item></channel></rss>